Skip to content

Commit ea5ceeb

Browse files
k-ibarakiclaude
andcommitted
fix: Address code review feedback
Apply all suggested improvements from code review: 1. Remove duplicate dict initialization in merged_cell_handler.py - Remove redundant None initialization (lines 39-40) - Add type hints at actual initialization (lines 73-74) 2. Narrow exception handling in range_calculator.py - Change `except Exception:` to `except ValueError:` for better specificity 3. Restore backward compatibility in calculate_range_size - Add try-except block to return (0, 0) on error - Maintain original behavior for invalid ranges - Add logging for debugging 4. Add defensive check for negative frozen_rows - Prevent negative values in pane_manager.py - Return (True, 0) for negative input 5. Restore duplicate normalization check in tests - Add mock-based call count verification - Ensure ExcelRangeCalculator methods called only once - Verify no performance regression from refactoring All 220 tests pass. Quality checks pass. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6b3f2bc commit ea5ceeb

6 files changed

Lines changed: 81 additions & 54 deletions

File tree

src/excel/merged_cell_handler.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ def build_merged_cell_cache(
3636
- merged_anchor_value_map: 結合範囲 -> アンカー値のマップ
3737
- merged_ranges: 結合範囲情報のリスト
3838
"""
39-
merged_cell_map: dict[str, str] | None = None
40-
merged_anchor_value_map: dict[str, Any] | None = None
4139
merged_ranges: list[dict[str, Any]] = []
4240

4341
# 今回返す予定の範囲(結合情報の部分展開に使用)
@@ -70,8 +68,8 @@ def build_merged_cell_cache(
7068
target_min_col = min(start_col_idx, end_col_idx)
7169
target_max_col = max(start_col_idx, end_col_idx)
7270

73-
merged_cell_map = {}
74-
merged_anchor_value_map = {}
71+
merged_cell_map: dict[str, str] = {}
72+
merged_anchor_value_map: dict[str, Any] = {}
7573

7674
for merged_range in sheet.merged_cells.ranges:
7775
merged_range_str = str(merged_range)

src/excel/pane_manager.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,12 @@ def validate_frozen_rows(frozen_rows: int, max_limit: int) -> tuple[bool, int]:
6767
6868
Returns:
6969
(is_valid, validated_frozen_rows)のタプル
70-
- is_valid: 上限以内ならTrue、超過ならFalse
71-
- validated_frozen_rows: 超過時は0、それ以外は元の値
70+
- is_valid: 上限以内ならTrue、超過または負の場合はFalse
71+
- validated_frozen_rows: 無効時は0、それ以外は元の値
7272
"""
73+
# 負の値は無効として0に丸める
74+
if frozen_rows < 0:
75+
return (True, 0)
7376
if frozen_rows > max_limit:
7477
return (False, 0)
7578
return (True, frozen_rows)

src/excel/range_calculator.py

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
セル範囲の計算・変換・検証を担当するヘルパークラス
55
"""
66

7+
import logging
8+
79
from openpyxl.utils import column_index_from_string, get_column_letter
810
from openpyxl.utils.cell import coordinate_from_string
911

12+
logger = logging.getLogger(__name__)
13+
1014

1115
class ExcelRangeCalculator:
1216
"""セル範囲の計算・変換・検証(全て staticmethod)"""
@@ -127,7 +131,7 @@ def expand_axis_range(range_str: str) -> str:
127131
try:
128132
col, row = coordinate_from_string(raw.replace("$", ""))
129133
return f"{col}1:{col}{row}"
130-
except Exception:
134+
except ValueError:
131135
return range_str
132136

133137
start_cell, end_cell = raw.split(":", 1)
@@ -160,33 +164,36 @@ def calculate_range_size(range_str: str) -> tuple[int, int]:
160164
range_str: セル範囲(例: "A1:D10" または "A1:XFD1048576")
161165
162166
Returns:
163-
(rows, cols)のタプル
164-
165-
Raises:
166-
ValueError: 逆順序の範囲を検出した場合
167+
(rows, cols)のタプル。
168+
エラー時は (0, 0) を返す。
167169
"""
168-
if ":" not in range_str:
169-
# 単一セルの場合
170-
return (1, 1)
171-
172-
start_cell, end_cell = range_str.split(":")
173-
start_col, start_row = coordinate_from_string(start_cell)
174-
end_col, end_row = coordinate_from_string(end_cell)
175-
176-
start_col_idx = column_index_from_string(start_col)
177-
end_col_idx = column_index_from_string(end_col)
178-
179-
# 逆順序の範囲を検出(セキュリティ対策)
180-
if end_row < start_row or end_col_idx < start_col_idx:
181-
raise ValueError(
182-
f"無効なセル範囲: '{range_str}'。"
183-
f"範囲は正しい順序で指定してください(例: 'A1:Z100')"
184-
)
185-
186-
rows = end_row - start_row + 1
187-
cols = end_col_idx - start_col_idx + 1
188-
189-
return (rows, cols)
170+
try:
171+
if ":" not in range_str:
172+
# 単一セルの場合
173+
return (1, 1)
174+
175+
start_cell, end_cell = range_str.split(":")
176+
start_col, start_row = coordinate_from_string(start_cell)
177+
end_col, end_row = coordinate_from_string(end_cell)
178+
179+
start_col_idx = column_index_from_string(start_col)
180+
end_col_idx = column_index_from_string(end_col)
181+
182+
# 逆順序の範囲を検出(セキュリティ対策)
183+
if end_row < start_row or end_col_idx < start_col_idx:
184+
raise ValueError(
185+
f"無効なセル範囲: '{range_str}'。"
186+
f"範囲は正しい順序で指定してください(例: 'A1:Z100')"
187+
)
188+
189+
rows = end_row - start_row + 1
190+
cols = end_col_idx - start_col_idx + 1
191+
192+
return (rows, cols)
193+
except Exception as e:
194+
# 元の実装との互換性維持: エラー時は (0, 0) を返す
195+
logger.warning(f"Failed to calculate range size '{range_str}': {e}")
196+
return (0, 0)
190197

191198
@staticmethod
192199
def normalize_column_range(cell_range: str, max_row: int) -> str:

tests/excel/test_excel_pane_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def test_validate_frozen_rows_zero(self):
175175
assert validated == 0
176176

177177
def test_validate_frozen_rows_negative_value(self):
178-
"""負の値の場合はTrueと元の値が返ること(実際には発生しない)"""
178+
"""負の値の場合はTrueと0が返ること(防御的処理)"""
179179
is_valid, validated = ExcelPaneManager.validate_frozen_rows(-1, 100)
180180
assert is_valid is True
181-
assert validated == -1
181+
assert validated == 0

tests/excel/test_excel_range_calculator.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,22 +127,22 @@ def test_calculate_range_size_single_column(self):
127127
assert cols == 1
128128

129129
def test_calculate_range_size_reverse_order_raises(self):
130-
"""逆順序の範囲でValueErrorが発生すること"""
131-
with pytest.raises(ValueError) as exc_info:
132-
ExcelRangeCalculator.calculate_range_size("D10:A1")
133-
assert "無効なセル範囲" in str(exc_info.value)
130+
"""逆順序の範囲で(0, 0)が返ること(互換性維持)"""
131+
rows, cols = ExcelRangeCalculator.calculate_range_size("D10:A1")
132+
assert rows == 0
133+
assert cols == 0
134134

135135
def test_calculate_range_size_reverse_column_raises(self):
136-
"""逆順序の列でValueErrorが発生すること"""
137-
with pytest.raises(ValueError) as exc_info:
138-
ExcelRangeCalculator.calculate_range_size("D1:A10")
139-
assert "無効なセル範囲" in str(exc_info.value)
136+
"""逆順序の列で(0, 0)が返ること(互換性維持)"""
137+
rows, cols = ExcelRangeCalculator.calculate_range_size("D1:A10")
138+
assert rows == 0
139+
assert cols == 0
140140

141141
def test_calculate_range_size_reverse_row_raises(self):
142-
"""逆順序の行でValueErrorが発生すること"""
143-
with pytest.raises(ValueError) as exc_info:
144-
ExcelRangeCalculator.calculate_range_size("A10:D1")
145-
assert "無効なセル範囲" in str(exc_info.value)
142+
"""逆順序の行で(0, 0)が返ること(互換性維持)"""
143+
rows, cols = ExcelRangeCalculator.calculate_range_size("A10:D1")
144+
assert rows == 0
145+
assert cols == 0
146146

147147
# normalize_column_range のテスト
148148

tests/test_sharepoint_excel.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -861,10 +861,15 @@ def test_normalize_column_range_already_normalized(self):
861861

862862
def test_no_duplicate_range_normalization(self):
863863
"""
864-
列範囲指定と拡張が正しく動作することを確認
864+
セル範囲の正規化・拡張が重複して実行されないことを確認
865865
866-
課題3-2の対応:範囲計算がヘルパークラスで一元化されたことを検証
866+
課題3-2の対応:_parse_sheetと_build_merged_cell_cacheで
867+
重複していた計算が1回のみになったことを検証
867868
"""
869+
from unittest.mock import patch
870+
871+
from src.excel import ExcelRangeCalculator
872+
868873
# テスト用Excelを作成(結合セルあり)
869874
wb = Workbook()
870875
ws = wb.active
@@ -888,11 +893,25 @@ def test_no_duplicate_range_normalization(self):
888893

889894
parser = SharePointExcelParser(self.mock_download_client)
890895

891-
# 列範囲指定で解析(expand_axis_range=Trueで拡張を有効化)
892-
result = parser.parse_to_json(
893-
"/test/file.xlsx", cell_range="A:B", expand_axis_range=True
894-
)
895-
result_data = json.loads(result)
896+
# 範囲計算メソッドの呼び出し回数を監視
897+
with patch.object(
898+
ExcelRangeCalculator,
899+
"normalize_column_range",
900+
wraps=ExcelRangeCalculator.normalize_column_range,
901+
) as mock_normalize, patch.object(
902+
ExcelRangeCalculator,
903+
"expand_axis_range",
904+
wraps=ExcelRangeCalculator.expand_axis_range,
905+
) as mock_expand:
906+
# 列範囲指定で解析(expand_axis_range=Trueで拡張を有効化)
907+
result = parser.parse_to_json(
908+
"/test/file.xlsx", cell_range="A:B", expand_axis_range=True
909+
)
910+
result_data = json.loads(result)
911+
912+
# 呼び出し回数が1回であることを確認(重複なし)
913+
assert mock_normalize.call_count == 1
914+
assert mock_expand.call_count == 1
896915

897916
# 結果が正しいことを確認
898917
assert "sheets" in result_data

0 commit comments

Comments
 (0)