@@ -1131,6 +1131,140 @@ def test_getitem_rejects_unsupported_key(self):
11311131 self .dataframe [42 ]
11321132
11331133
1134+ class DataFrameGetAttrTests (PyFlinkDataFrameUTTestCase ):
1135+ def setUp (self ):
1136+ super ().setUp ()
1137+ self .dataframe = pf .from_records (
1138+ [(1 , "Alice" ), (2 , "Bob" )], schema = ["id" , "name" ]
1139+ )
1140+
1141+ def test_getattr_returns_column_expression (self ):
1142+ self .assertIsInstance (self .dataframe .name , Expression )
1143+ self .assertEqual (str (self .dataframe .name ), str (self .dataframe ["name" ]))
1144+ self .assert_dataframe_schema (
1145+ self .dataframe .select (self .dataframe .name ), ["name" ]
1146+ )
1147+
1148+ def test_getattr_composes_with_filter_and_select (self ):
1149+ result = self .dataframe .filter (lambda df : df .id > 1 ).select (
1150+ self .dataframe .name , (self .dataframe .id + 1 ).alias ("next_id" )
1151+ )
1152+ self .assert_dataframe_schema (result , ["name" , "next_id" ])
1153+
1154+ def test_missing_column_raises_attribute_error (self ):
1155+ with self .assertRaisesRegex (AttributeError , "missing" ):
1156+ self .dataframe .missing
1157+ self .assertFalse (hasattr (self .dataframe , "missing" ))
1158+ default = object ()
1159+ self .assertIs (getattr (self .dataframe , "missing" , default ), default )
1160+ self .assertTrue (hasattr (self .dataframe , "name" ))
1161+
1162+ def test_existing_attributes_take_precedence_over_columns (self ):
1163+ names = ["select" , "filter" , "columns" , "schema" , "_table" , "__class__" ]
1164+ dataframe = pf .from_records ([tuple (range (len (names )))], schema = names )
1165+ self .assertIs (dataframe .select .__func__ , pf .DataFrame .select )
1166+ self .assertIs (dataframe .filter .__func__ , pf .DataFrame .filter )
1167+ self .assertEqual (dataframe .columns , names )
1168+ self .assertIsInstance (dataframe .schema , TableSchema )
1169+ self .assertIs (dataframe ._table , dataframe .to_table ())
1170+ self .assertIs (dataframe .__class__ , pf .DataFrame )
1171+ for name in names :
1172+ with self .subTest (name = name ):
1173+ self .assert_dataframe_schema (dataframe .select (dataframe [name ]), [name ])
1174+
1175+ def test_instance_attributes_take_precedence_over_columns (self ):
1176+ marker = object ()
1177+ self .dataframe .name = marker
1178+ self .assertIs (self .dataframe .name , marker )
1179+ self .assertIsInstance (self .dataframe ["name" ], Expression )
1180+
1181+ def test_invalid_identifiers_and_keywords_are_not_attributes (self ):
1182+ for name in ("first name" , "first-name" , "1name" , "class" , "None" ):
1183+ with self .subTest (name = name ):
1184+ dataframe = pf .from_records ([(1 ,)], schema = [name ])
1185+ with self .assertRaises (AttributeError ):
1186+ getattr (dataframe , name )
1187+ self .assertIsInstance (dataframe [name ], Expression )
1188+
1189+ def test_valid_identifiers_include_unicode_and_digits (self ):
1190+ for name in ("name_2" , "\u540d \u5b57 " , "match" ):
1191+ with self .subTest (name = name ):
1192+ dataframe = pf .from_records ([(1 ,)], schema = [name ])
1193+ self .assert_dataframe_schema (dataframe .select (getattr (dataframe , name )), [name ])
1194+
1195+ def test_private_names_require_bracket_access (self ):
1196+ names = ["_name" , "__dataframe__" , "__deepcopy__" , "_repr_html_" ]
1197+ dataframe = pf .from_records ([tuple (range (len (names )))], schema = names )
1198+
1199+ for name in names :
1200+ with self .subTest (name = name ):
1201+ with self .assertRaises (AttributeError ):
1202+ getattr (dataframe , name )
1203+ self .assertFalse (hasattr (dataframe , name ))
1204+ self .assert_dataframe_schema (dataframe .select (dataframe [name ]), [name ])
1205+
1206+ def test_getattr_uses_the_transformed_schema (self ):
1207+ renamed = self .dataframe .rename_columns ({"name" : "label" })
1208+ self .assertIsInstance (renamed .label , Expression )
1209+ self .assertFalse (hasattr (renamed , "name" ))
1210+ projected = self .dataframe .select ("id" )
1211+ self .assertFalse (hasattr (projected , "name" ))
1212+ self .assertIsInstance (self .dataframe .name , Expression )
1213+
1214+
1215+ class DataFrameGetAttrValidationTests (unittest .TestCase ):
1216+ def test_invalid_names_do_not_resolve_schema (self ):
1217+ table = Mock ()
1218+ for name in (
1219+ "" ,
1220+ "first name" ,
1221+ "class" ,
1222+ "_name" ,
1223+ "__dataframe__" ,
1224+ "__deepcopy__" ,
1225+ "_repr_html_" ,
1226+ ):
1227+ with self .subTest (name = name ):
1228+ with self .assertRaises (AttributeError ):
1229+ getattr (pf .DataFrame (table ), name )
1230+ table .get_resolved_schema .assert_not_called ()
1231+
1232+ def test_failing_class_descriptor_does_not_fall_back_to_column (self ):
1233+ class DerivedDataFrame (pf .DataFrame ):
1234+ @property
1235+ def name (self ):
1236+ raise AttributeError ("descriptor unavailable" )
1237+
1238+ table = Mock ()
1239+ table .get_resolved_schema .return_value .get_column_names .return_value = ["name" ]
1240+
1241+ with self .assertRaises (AttributeError ):
1242+ DerivedDataFrame (table ).name
1243+ table .get_resolved_schema .assert_not_called ()
1244+
1245+ def test_uninitialized_dataframe_does_not_recurse (self ):
1246+ dataframe = object .__new__ (pf .DataFrame )
1247+ for name in ("_table" , "name" , "__setstate__" ):
1248+ with self .subTest (name = name ):
1249+ with self .assertRaises (AttributeError ):
1250+ getattr (dataframe , name )
1251+
1252+ def test_column_access_does_not_execute_a_job (self ):
1253+ table = Mock ()
1254+ table .get_resolved_schema .return_value .get_column_names .return_value = ["name" ]
1255+ expression = object ()
1256+ with patch ("pyflink.dataframe.dataframe.table_col" , return_value = expression ) as col :
1257+ self .assertIs (pf .DataFrame (table ).name , expression )
1258+ col .assert_called_once_with ("name" )
1259+ table .execute .assert_not_called ()
1260+
1261+ def test_schema_errors_are_not_hidden (self ):
1262+ table = Mock ()
1263+ table .get_resolved_schema .side_effect = RuntimeError ("schema unavailable" )
1264+ with self .assertRaisesRegex (RuntimeError , "schema unavailable" ):
1265+ pf .DataFrame (table ).name
1266+
1267+
11341268class DataFrameLiteralTests (PyFlinkDataFrameUTTestCase ):
11351269 def setUp (self ):
11361270 super ().setUp ()
@@ -2304,6 +2438,15 @@ def setUp(self):
23042438 self .addCleanup (pf .set_table_environment , previous_environment )
23052439 self .t_env = TableEnvironment .create (EnvironmentSettings .in_batch_mode ())
23062440
2441+ def test_attribute_column_access_executes (self ):
2442+ dataframe = pf .from_table (self .t_env .sql_query (
2443+ "SELECT * FROM (VALUES (1, 'Alice'), (2, 'Bob')) AS T(id, name)"
2444+ ))
2445+ result = dataframe .filter (lambda df : df .id > 1 ).select (
2446+ dataframe .name , (dataframe .id + 10 ).alias ("next_id" )
2447+ )
2448+ self .assertEqual (result .collect (), [Row ("Bob" , 12 )])
2449+
23072450 def _ordered_dataframe (self ):
23082451 table = self .t_env .sql_query (
23092452 "SELECT * FROM (VALUES (3, 'C'), (1, 'A'), (4, 'D'), (2, 'B')) "
0 commit comments