Skip to content

Commit 66e8488

Browse files
committed
fix: handle complex bitable field emptiness
1 parent edf4037 commit 66e8488

3 files changed

Lines changed: 76 additions & 10 deletions

File tree

core/converter.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ def _normalize_index_value(
225225
return self._normalize_index_value(value.get("value"), nested_type)
226226

227227
if "text" in value:
228-
return str(value.get("text", ""))
228+
text_value = str(value.get("text", ""))
229+
return text_value if text_value.strip() else None
229230

230231
if "link_record_ids" in value:
231232
return self._normalize_index_value(
@@ -261,7 +262,9 @@ def _normalize_index_value(
261262
text_parts = []
262263
for item in values:
263264
if isinstance(item, dict) and "text" in item:
264-
text_parts.append(str(item.get("text", "")))
265+
text_value = str(item.get("text", ""))
266+
if text_value.strip():
267+
text_parts.append(text_value)
265268
else:
266269
item_value = self._normalize_index_value(item)
267270
if item_value is not None:
@@ -919,11 +922,11 @@ def convert_field_value_safe(
919922
self, field_name: str, value, field_types: Optional[Dict[str, int]] = None
920923
):
921924
"""安全的字段值转换"""
922-
if pd.isnull(value):
923-
return None
924-
925925
# 多维表格模式使用复杂转换
926926
if self.target_type == TargetType.BITABLE:
927+
if self._is_empty_value(value):
928+
return None
929+
927930
# 如果没有字段类型信息,使用智能转换
928931
if field_types is None or field_name not in field_types:
929932
return self.smart_convert_value(value)
@@ -948,6 +951,13 @@ def convert_field_value_safe(
948951
self.conversion_stats["failed"] += 1
949952
return None
950953
else:
954+
if pd.api.types.is_scalar(value):
955+
try:
956+
if pd.isnull(value):
957+
return None
958+
except (TypeError, ValueError):
959+
pass
960+
951961
# 电子表格模式使用简单转换
952962
return self.simple_convert_value(value)
953963

@@ -1249,7 +1259,7 @@ def simple_convert_value(self, value):
12491259

12501260
def convert_to_user_field(self, value):
12511261
"""转换为人员字段格式"""
1252-
if pd.isnull(value) or not value:
1262+
if self._is_empty_value(value):
12531263
return None
12541264

12551265
# 如果已经是正确的字典格式
@@ -1280,7 +1290,7 @@ def convert_to_user_field(self, value):
12801290

12811291
def convert_to_url_field(self, value):
12821292
"""转换为超链接字段格式"""
1283-
if pd.isnull(value) or not value:
1293+
if self._is_empty_value(value):
12841294
return None
12851295

12861296
# 如果已经是正确的字典格式
@@ -1299,7 +1309,7 @@ def convert_to_url_field(self, value):
12991309

13001310
def convert_to_attachment_field(self, value):
13011311
"""转换为附件字段格式"""
1302-
if pd.isnull(value) or not value:
1312+
if self._is_empty_value(value):
13031313
return None
13041314

13051315
# 如果已经是正确的字典格式
@@ -1322,7 +1332,7 @@ def convert_to_attachment_field(self, value):
13221332

13231333
def convert_to_link_field(self, value):
13241334
"""转换为关联字段格式"""
1325-
if pd.isnull(value) or not value:
1335+
if self._is_empty_value(value):
13261336
return None
13271337

13281338
# 如果已经是列表格式

core/engine.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2128,7 +2128,10 @@ def sync(self, df: pd.DataFrame) -> bool:
21282128

21292129
for _, row in df.head(sample_size).iterrows():
21302130
for col_name, value in row.to_dict().items():
2131-
if pd.notnull(value) and col_name in field_types:
2131+
if (
2132+
not self.converter._is_empty_value(value)
2133+
and col_name in field_types
2134+
):
21322135
field_type = field_types[col_name]
21332136
# 简单的类型不匹配检测
21342137
if field_type == 2 and isinstance(

tests/test_converter.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@ def test_get_index_value_hash_text_list_matches_plain_text(self):
180180

181181
assert plain_hash == text_list_hash
182182

183+
def test_get_index_value_hash_empty_rich_text_is_empty(self):
184+
"""测试空富文本不会生成空字符串哈希"""
185+
converter = DataConverter(TargetType.BITABLE)
186+
rich_text_row = pd.Series({"ID": [{"text": "", "type": "text"}]})
187+
188+
hash_value = converter.get_index_value_hash(rich_text_row, "ID", {"ID": 1})
189+
190+
assert hash_value is None
191+
183192
def test_get_index_value_hash_date_string_matches_timestamp(self):
184193
"""测试本地日期字符串和飞书日期时间戳生成相同索引哈希"""
185194
converter = DataConverter(TargetType.BITABLE)
@@ -522,6 +531,32 @@ def test_convert_without_field_types(self):
522531
result = converter.convert_field_value_safe("test", "123", None)
523532
assert result == 123 # 智能识别为数字
524533

534+
def test_convert_multi_segment_text_value(self):
535+
"""测试多段富文本写入文本字段时不会触发 pandas 空值判断异常"""
536+
converter = DataConverter(TargetType.BITABLE)
537+
value = [
538+
{"text": "李宁少昊", "type": "text"},
539+
{"text": "运动户外专卖店", "type": "text"},
540+
]
541+
542+
result = converter.convert_field_value_safe("Name", value, {"Name": 1})
543+
544+
assert result == "李宁少昊运动户外专卖店"
545+
546+
def test_convert_multi_value_complex_fields(self):
547+
"""测试多个复杂字段值写入时不会触发 pandas 空值判断异常"""
548+
converter = DataConverter(TargetType.BITABLE)
549+
550+
assert converter.convert_field_value_safe(
551+
"Users", ["ou_1", "ou_2"], {"Users": 11}
552+
) == [{"id": "ou_1"}, {"id": "ou_2"}]
553+
assert converter.convert_field_value_safe(
554+
"Files", ["file_1", "file_2"], {"Files": 17}
555+
) == [{"file_token": "file_1"}, {"file_token": "file_2"}]
556+
assert converter.convert_field_value_safe(
557+
"Links", ["rec_1", "rec_2"], {"Links": 18}
558+
) == ["rec_1", "rec_2"]
559+
525560

526561
class TestSimpleConvertValue:
527562
"""简单值转换测试(电子表格模式)"""
@@ -683,6 +718,24 @@ def test_df_to_records_preserves_complex_text_value(self):
683718

684719
assert records == [{"fields": {"Name": "李宁少昊运动户外专卖店"}}]
685720

721+
def test_df_to_records_preserves_multi_segment_text_value(self):
722+
"""测试多段富文本写入不会被 pandas 空值判断中断"""
723+
converter = DataConverter(TargetType.BITABLE)
724+
df = pd.DataFrame(
725+
{
726+
"Name": [
727+
[
728+
{"text": "李宁少昊", "type": "text"},
729+
{"text": "运动户外专卖店", "type": "text"},
730+
],
731+
]
732+
}
733+
)
734+
735+
records = converter.df_to_records(df, {"Name": 1})
736+
737+
assert records == [{"fields": {"Name": "李宁少昊运动户外专卖店"}}]
738+
686739
def test_df_to_records_sheet_raises_error(self, sample_dataframe):
687740
"""测试电子表格模式调用 df_to_records 抛出错误"""
688741
converter = DataConverter(TargetType.SHEET)

0 commit comments

Comments
 (0)