Repository: dask/dask

Issue:
`test_filter_nonpartition_columns` flaky
Over in https://github.com/dask/dask/pull/9017 we observed `test_filter_nonpartition_columns[fastparquet-fastparquet-None]` fail  in [this CI build](https://github.com/dask/dask/runs/6306465028?check_suite_focus=true) with

```python
    @classmethod
    def write_partition(
        cls,
        df,
        path,
        fs,
        filename,
        partition_on,
        return_metadata,
        fmd=None,
        compression=None,
        custom_metadata=None,
        **kwargs,
    ):
        # Update key/value metadata if necessary
        fmd = copy.copy(fmd)
        for s in fmd.schema:
            if isinstance(s.name, bytes):
                # can be coerced to bytes on copy
>               s.name = s.name.decode()
E               AttributeError: 'str' object has no attribute 'decode'
```

<details>
<summary>Full traceback:</summary>

```
_______ test_filter_nonpartition_columns[fastparquet-fastparquet-None] ________
[gw0] win32 -- Python 3.10.4 C:\Miniconda3\envs\test-environment\python.exe

tmpdir = 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\pytest-of-runneradmin\\pytest-0\\popen-gw0\\test_filter_nonpartition_colum0'
write_engine = 'fastparquet', read_engine = 'fastparquet'
calculate_divisions = None

    @pytest.mark.parametrize("calculate_divisions", [None, True])
    @write_read_engines()
    def test_filter_nonpartition_columns(
        tmpdir, write_engine, read_engine, calculate_divisions
    ):
        tmpdir = str(tmpdir)
        df_write = pd.DataFrame(
            {
                "id": [1, 2, 3, 4] * 4,
                "time": np.arange(16),
                "random": np.random.choice(["cat", "dog"], size=16),
            }
        )
        ddf_write = dd.from_pandas(df_write, npartitions=4)
>       ddf_write.to_parquet(
            tmpdir, write_index=False, partition_on=["id"], engine=write_engine
        )

dask\dataframe\io\tests\test_parquet.py:2978: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dask\dataframe\core.py:4990: in to_parquet
    return to_parquet(self, path, *args, **kwargs)
dask\dataframe\io\parquet\core.py:891: in to_parquet
    out = out.compute(**compute_kwargs)
dask\base.py:292: in compute
    (result,) = compute(self, traverse=False, **kwargs)
dask\base.py:575: in compute
    results = schedule(dsk, keys, **kwargs)
dask\threaded.py:81: in get
    results = get_async(
dask\local.py:508: in get_async
    raise_exception(exc, tb)
dask\local.py:316: in reraise
    raise exc
dask\local.py:221: in execute_task
    result = _execute_task(task, data)
dask\core.py:119: in _execute_task
    return func(*(_execute_task(a, cache) for a in args))
dask\optimization.py:990: in __call__
    return core.get(self.dsk, self.outkey, dict(zip(self.inkeys, args)))
dask\core.py:149: in get
    result = _execute_task(task, cache)
dask\core.py:119: in _execute_task
    return func(*(_execute_task(a, cache) for a in args))
dask\dataframe\io\parquet\core.py:154: in __call__
    return self.engine.write_partition(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

cls = <class 'dask.dataframe.io.parquet.fastparquet.FastParquetEngine'>
df =    id  time random
0   1     0    dog
1   2     1    cat
2   3     2    cat
3   4     3    dog
path = 'C:/Users/runneradmin/AppData/Local/Temp/pytest-of-runneradmin/pytest-0/popen-gw0/test_filter_nonpartition_colum0'
fs = <fsspec.implementations.local.LocalFileSystem object at 0x00000252337A1F60>
filename = 'part.0.parquet', partition_on = ['id'], return_metadata = None
fmd = column_orders: null
created_by: b'fastparquet-python version 1.0.0 (build 111)'
encryption_algorithm: null
footer_sign...dom
  num_children: null
  precision: null
  repetition_type: 1
  scale: null
  type: 6
  type_length: null
version: 1

compression = 'snappy', custom_metadata = None, kwargs = {'head': True}
s = converted_type: null
field_id: null
logicalType: null
name: schema
num_children: 2
precision: null
repetition_type: null
scale: null
type: null
type_length: null


    @classmethod
    def write_partition(
        cls,
        df,
        path,
        fs,
        filename,
        partition_on,
        return_metadata,
        fmd=None,
        compression=None,
        custom_metadata=None,
        **kwargs,
    ):
        # Update key/value metadata if necessary
        fmd = copy.copy(fmd)
        for s in fmd.schema:
            if isinstance(s.name, bytes):
                # can be coerced to bytes on copy
>               s.name = s.name.decode()
E               AttributeError: 'str' object has no attribute 'decode'

dask\dataframe\io\parquet\fastparquet.py:1223: AttributeError
```

</details>

cc @martindurant if you get a chance to take a look 


Discussion:
It is conceivable for the attribute to be bytes or str; but this must be a race given that we only just checked the line above. Would it be better to try decode and catch the AttributeError? Or, alternatively extract `name = s.name` before the check.
Try and catch makes sense to me. 

Resolving patch:
diff --git a/dask/dataframe/io/parquet/fastparquet.py b/dask/dataframe/io/parquet/fastparquet.py
--- a/dask/dataframe/io/parquet/fastparquet.py
+++ b/dask/dataframe/io/parquet/fastparquet.py
@@ -1226,9 +1226,11 @@ def write_partition(
         # Update key/value metadata if necessary
         fmd = copy.copy(fmd)
         for s in fmd.schema:
-            if isinstance(s.name, bytes):
+            try:
                 # can be coerced to bytes on copy
                 s.name = s.name.decode()
+            except AttributeError:
+                pass
         if custom_metadata and fmd is not None:
             fmd.key_value_metadata = fmd.key_value_metadata + (
                 [


Test patch: