@@ -118,6 +118,8 @@ def __init__(
118
118
):
119
119
global bigframes
120
120
121
+ self ._query_job : Optional [bigquery .QueryJob ] = None
122
+
121
123
if copy is not None and not copy :
122
124
raise ValueError (
123
125
f"DataFrame constructor only supports copy=True. { constants .FEEDBACK_LINK } "
@@ -182,7 +184,6 @@ def __init__(
182
184
if dtype :
183
185
bf_dtype = bigframes .dtypes .bigframes_type (dtype )
184
186
block = block .multi_apply_unary_op (ops .AsTypeOp (to_type = bf_dtype ))
185
- self ._block = block
186
187
187
188
else :
188
189
import bigframes .pandas
@@ -194,10 +195,14 @@ def __init__(
194
195
dtype = dtype , # type:ignore
195
196
)
196
197
if session :
197
- self . _block = session .read_pandas (pd_dataframe )._get_block ()
198
+ block = session .read_pandas (pd_dataframe )._get_block ()
198
199
else :
199
- self ._block = bigframes .pandas .read_pandas (pd_dataframe )._get_block ()
200
- self ._query_job : Optional [bigquery .QueryJob ] = None
200
+ block = bigframes .pandas .read_pandas (pd_dataframe )._get_block ()
201
+
202
+ # We use _block as an indicator in __getattr__ and __setattr__ to see
203
+ # if the object is fully initialized, so make sure we set the _block
204
+ # attribute last.
205
+ self ._block = block
201
206
self ._block .session ._register_object (self )
202
207
203
208
def __dir__ (self ):
@@ -625,13 +630,17 @@ def _getitem_bool_series(self, key: bigframes.series.Series) -> DataFrame:
625
630
return DataFrame (block )
626
631
627
632
def __getattr__ (self , key : str ):
628
- # Protect against recursion errors with uninitialized DataFrame
629
- # objects. See:
633
+ # To allow subclasses to set private attributes before the class is
634
+ # fully initialized, protect against recursion errors with
635
+ # uninitialized DataFrame objects. Note: this comes at the downside
636
+ # that columns with a leading `_` won't be treated as columns.
637
+ #
638
+ # See:
630
639
# https://github.com/googleapis/python-bigquery-dataframes/issues/728
631
640
# and
632
641
# https://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html
633
642
if key == "_block" :
634
- raise AttributeError ("_block" )
643
+ raise AttributeError (key )
635
644
636
645
if key in self ._block .column_labels :
637
646
return self .__getitem__ (key )
@@ -651,26 +660,36 @@ def __getattr__(self, key: str):
651
660
raise AttributeError (key )
652
661
653
662
def __setattr__ (self , key : str , value ):
654
- if key in ["_block" , "_query_job" ]:
663
+ if key == "_block" :
664
+ object .__setattr__ (self , key , value )
665
+ return
666
+
667
+ # To allow subclasses to set private attributes before the class is
668
+ # fully initialized, assume anything set before `_block` is initialized
669
+ # is a regular attribute.
670
+ if not hasattr (self , "_block" ):
655
671
object .__setattr__ (self , key , value )
656
672
return
657
- # Can this be removed???
673
+
674
+ # If someone has a column named the same as a normal attribute
675
+ # (e.g. index), we want to set the normal attribute, not the column.
676
+ # To do that, check if there is a normal attribute by using
677
+ # __getattribute__ (not __getattr__, because that includes columns).
678
+ # If that returns a value without raising, then we know this is a
679
+ # normal attribute and we should prefer that.
658
680
try :
659
- # boring attributes go through boring old path
660
681
object .__getattribute__ (self , key )
661
682
return object .__setattr__ (self , key , value )
662
683
except AttributeError :
663
684
pass
664
685
665
- # if this fails, go on to more involved attribute setting
666
- # (note that this matches __getattr__, above).
667
- try :
668
- if key in self .columns :
669
- self [key ] = value
670
- else :
671
- object .__setattr__ (self , key , value )
672
- # Can this be removed?
673
- except (AttributeError , TypeError ):
686
+ # If we made it here, then we know that it's not a regular attribute
687
+ # already, so it might be a column to update. Note: we don't allow
688
+ # adding new columns using __setattr__, only __setitem__, that way we
689
+ # can still add regular new attributes.
690
+ if key in self ._block .column_labels :
691
+ self [key ] = value
692
+ else :
674
693
object .__setattr__ (self , key , value )
675
694
676
695
def __repr__ (self ) -> str :
0 commit comments