Repository: pandas-dev/pandas (python)

Issue:
BUG: Chained urls (allowed by fsspec) not recognized by is_fsspec_url
### Pandas version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the [latest version](https://pandas.pydata.org/docs/whatsnew/index.html) of pandas.

- [X] I have confirmed this bug exists on the main branch of pandas.


### Reproducible Example

```python
import pandas as pd
from pandas.io.common import is_fsspec_url
import fsspec

# This fsspec trick chains a cache fs with an s3 file system using "::".
# See https://filesystem-spec.readthedocs.io/en/latest/features.html#url-chaining.
fn = "filecache::s3://ncei-wcsd-archive/data/processed/SH1305/18kHz/SaKe2013-D20130523-T080854_to_SaKe2013-D20130523-T085643.csv"

with fsspec.open(fn, storage_options={"s3": {"anon": True}}) as f:
    foo = pd.read_csv(f)

print(foo.shape)

# But this url isn't recognized as a valid fsspec url by pandas...
print(pd.io.common.is_fsspec_url(fn))

# ...and so attempts to use chained url directly fail:
bar = pd.read_csv("filecache::s3://ncei-wcsd-archive/data/processed/SH1305/18kHz/"
                 "SaKe2013-D20130523-T080854_to_SaKe2013-D20130523-T085643.csv",
                 storage_options={"s3": {"anon": True},
                                  "filecache":{"cache_storage":"/tmp/cache"}})
```


### Issue Description

fsspec allows one to pass "chained" urls (useful, e.g., for caching), for example "filecache::https://example.com/my_file".  However, since commit eeff2b0c3b meant to address issue #36271 supplying urls of this form to e.g., pd.read_csv has failed.

### Expected Behavior

Pandas should pass the chained url to fsspec.  In the example code, pd.Dataframes foo and bar should be identical.

### Installed Versions

<details>
INSTALLED VERSIONS
------------------
commit           : 87cfe4e38bafe7300a6003a1d18bd80f3f77c763
python           : 3.9.12.final.0
python-bits      : 64
OS               : Linux
OS-release       : 5.10.131-19115-g2e2fb0ed324d
Version          : #1 SMP PREEMPT Mon Sep 12 18:55:51 PDT 2022
machine          : x86_64
processor        :
byteorder        : little
LC_ALL           : None
LANG             : en_US.UTF-8
LOCALE           : en_US.UTF-8

pandas           : 1.5.0
numpy            : 1.23.3
pytz             : 2022.4
dateutil         : 2.8.2
setuptools       : 58.1.0
pip              : 22.2.2
Cython           : None
pytest           : None
hypothesis       : None
sphinx           : None
blosc            : None
feather          : None
xlsxwriter       : None
lxml.etree       : None
html5lib         : None
pymysql          : None
psycopg2         : None
jinja2           : None
IPython          : 8.5.0
pandas_datareader: None
bs4              : None
bottleneck       : None
brotli           : None
fastparquet      : None
fsspec           : 2022.8.2
gcsfs            : None
matplotlib       : None
numba            : None
numexpr          : None
odfpy            : None
openpyxl         : None
pandas_gbq       : None
pyarrow          : None
pyreadstat       : None
pyxlsb           : None
s3fs             : 2022.8.2
scipy            : None
snappy           : None
sqlalchemy       : None
tables           : None
tabulate         : None
xarray           : None
xlrd             : None
xlwt             : None
zstandard        : None
tzdata           : None


</details>


Pull request description:
BUG: Recognize chained fsspec URLs
- [ ] closes #48978
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/v3.0.0.rst` file if fixing a bug or adding a new feature.

The new `(::[A-Za-z0-9+\-+.]+)*` part of the regex will allow for optional `::<alphanumeric+-.>`-like strings  before the `://` section of the URL, which will enable it to recognize chained fsspec URLs.

Patch:
diff --git a/.github/workflows/docbuild-and-upload.yml b/.github/workflows/docbuild-and-upload.yml
index 908baa8781..294334ca1d 100644
--- a/.github/workflows/docbuild-and-upload.yml
+++ b/.github/workflows/docbuild-and-upload.yml
@@ -59,6 +59,10 @@ jobs:
     - name: Build documentation
       run: doc/make.py --warnings-are-errors
 
+    - name: Build the interactive terminal
+      working-directory: web/interactive_terminal
+      run: jupyter lite build
+
     - name: Build documentation zip
       run: doc/make.py zip_html
 
diff --git a/Dockerfile b/Dockerfile
index dead3a494e..4090a4adb1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -13,5 +13,5 @@ COPY requirements-dev.txt /tmp
 RUN python -m pip install -r /tmp/requirements-dev.txt
 RUN git config --global --add safe.directory /home/pandas
 
-ENV SHELL "/bin/bash"
+ENV SHELL="/bin/bash"
 CMD ["/bin/bash"]
diff --git a/doc/source/user_guide/io.rst b/doc/source/user_guide/io.rst
index 07d06f61b3..23da52f263 100644
--- a/doc/source/user_guide/io.rst
+++ b/doc/source/user_guide/io.rst
@@ -18,10 +18,10 @@ The pandas I/O API is a set of top level ``reader`` functions accessed like
     :widths: 30, 100, 60, 60
 
     text,`CSV <https://en.wikipedia.org/wiki/Comma-separated_values>`__, :ref:`read_csv<io.read_csv_table>`, :ref:`to_csv<io.store_in_csv>`
-    text,Fixed-Width Text File, :ref:`read_fwf<io.fwf_reader>` , NA
+    text,Fixed-Width Text File, :ref:`read_fwf<io.fwf_reader>`, NA
     text,`JSON <https://www.json.org/>`__, :ref:`read_json<io.json_reader>`, :ref:`to_json<io.json_writer>`
     text,`HTML <https://en.wikipedia.org/wiki/HTML>`__, :ref:`read_html<io.read_html>`, :ref:`to_html<io.html>`
-    text,`LaTeX <https://en.wikipedia.org/wiki/LaTeX>`__, :ref:`Styler.to_latex<io.latex>` , NA
+    text,`LaTeX <https://en.wikipedia.org/wiki/LaTeX>`__, NA, :ref:`Styler.to_latex<io.latex>`
     text,`XML <https://www.w3.org/standards/xml/core>`__, :ref:`read_xml<io.read_xml>`, :ref:`to_xml<io.xml>`
     text, Local clipboard, :ref:`read_clipboard<io.clipboard>`, :ref:`to_clipboard<io.clipboard>`
     binary,`MS Excel <https://en.wikipedia.org/wiki/Microsoft_Excel>`__ , :ref:`read_excel<io.excel_reader>`, :ref:`to_excel<io.excel_writer>`
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 912b658cff..0df5a70d87 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -75,6 +75,7 @@ Other enhancements
 - Added half-year offset classes :class:`HalfYearBegin`, :class:`HalfYearEnd`, :class:`BHalfYearBegin` and :class:`BHalfYearEnd` (:issue:`60928`)
 - Errors occurring during SQL I/O will now throw a generic :class:`.DatabaseError` instead of the raw Exception type from the underlying driver manager library (:issue:`60748`)
 - Implemented :meth:`Series.str.isascii` and :meth:`Series.str.isascii` (:issue:`59091`)
+- Improved deprecation message for offset aliases (:issue:`60820`)
 - Multiplying two :class:`DateOffset` objects will now raise a ``TypeError`` instead of a ``RecursionError`` (:issue:`59442`)
 - Restore support for reading Stata 104-format and enable reading 103-format dta files (:issue:`58554`)
 - Support passing a :class:`Iterable[Hashable]` input to :meth:`DataFrame.drop_duplicates` (:issue:`59237`)
@@ -717,6 +718,7 @@ I/O
 ^^^
 - Bug in :class:`DataFrame` and :class:`Series` ``repr`` of :py:class:`collections.abc.Mapping`` elements. (:issue:`57915`)
 - Bug in :meth:`.DataFrame.to_json` when ``"index"`` was a value in the :attr:`DataFrame.column` and :attr:`Index.name` was ``None``. Now, this will fail with a ``ValueError`` (:issue:`58925`)
+- Bug in :meth:`.io.common.is_fsspec_url` not recognizing chained fsspec URLs (:issue:`48978`)
 - Bug in :meth:`DataFrame._repr_html_` which ignored the ``"display.float_format"`` option (:issue:`59876`)
 - Bug in :meth:`DataFrame.from_records` where ``columns`` parameter with numpy structured array was not reordering and filtering out the columns (:issue:`59717`)
 - Bug in :meth:`DataFrame.to_dict` raises unnecessary ``UserWarning`` when columns are not unique and ``orient='tight'``. (:issue:`58281`)
diff --git a/environment.yml b/environment.yml
index 69647a436e..a8c8b20e20 100644
--- a/environment.yml
+++ b/environment.yml
@@ -116,6 +116,13 @@ dependencies:
   - requests
   - pygments # Code highlighting
 
+  # web interactive REPL
+  # see the following links for more context:
+  # 1. https://jupyterlite-pyodide-kernel.readthedocs.io/en/stable/#compatibility
+  # 2. https://pyodide.org/en/stable/usage/packages-in-pyodide.html
+  - jupyterlite-core
+  - jupyterlite-pyodide-kernel
+
   - pip:
       - adbc-driver-postgresql>=0.10.0
       - adbc-driver-sqlite>=0.8.0
diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx
index ecd675dd89..a16964435e 100644
--- a/pandas/_libs/tslibs/offsets.pyx
+++ b/pandas/_libs/tslibs/offsets.pyx
@@ -5108,7 +5108,7 @@ def _warn_about_deprecated_aliases(name: str, is_period: bool) -> str:
         warnings.warn(
             f"\'{name}\' is deprecated and will be removed "
             f"in a future version, please use "
-            f"\'{c_PERIOD_AND_OFFSET_DEPR_FREQSTR.get(name)}\' "
+            f"\'{c_PERIOD_AND_OFFSET_DEPR_FREQSTR.get(name)}\'"
             f" instead.",
             FutureWarning,
             stacklevel=find_stack_level(),
@@ -5122,7 +5122,7 @@ def _warn_about_deprecated_aliases(name: str, is_period: bool) -> str:
             warnings.warn(
                 f"\'{name}\' is deprecated and will be removed "
                 f"in a future version, please use "
-                f"\'{_name}\' "
+                f"\'{_name}\'"
                 f" instead.",
                 FutureWarning,
                 stacklevel=find_stack_level(),
diff --git a/pandas/compat/pyarrow.py b/pandas/compat/pyarrow.py
index c501c06b93..163934bee5 100644
--- a/pandas/compat/pyarrow.py
+++ b/pandas/compat/pyarrow.py
@@ -19,6 +19,7 @@ try:
     pa_version_under17p0 = _palv < Version("17.0.0")
     pa_version_under18p0 = _palv < Version("18.0.0")
     pa_version_under19p0 = _palv < Version("19.0.0")
+    pa_version_under20p0 = _palv < Version("20.0.0")
     HAS_PYARROW = True
 except ImportError:
     pa_version_under10p1 = True
@@ -32,4 +33,5 @@ except ImportError:
     pa_version_under17p0 = True
     pa_version_under18p0 = True
     pa_version_under19p0 = True
+    pa_version_under20p0 = True
     HAS_PYARROW = False
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 4a86048bc2..28485bf5fd 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -5880,6 +5880,8 @@ class DataFrame(NDFrame, OpsMixin):
             Delete columns to be used as the new index.
         append : bool, default False
             Whether to append columns to existing index.
+            Setting to True will add the new columns to existing index.
+            When set to False, the current index will be dropped from the DataFrame.
         inplace : bool, default False
             Whether to modify the DataFrame rather than creating a new one.
         verify_integrity : bool, default False
@@ -5953,6 +5955,25 @@ class DataFrame(NDFrame, OpsMixin):
         2 4       4  2014    40
         3 9       7  2013    84
         4 16     10  2014    31
+
+        Append a column to the existing index:
+
+        >>> df = df.set_index("month")
+        >>> df.set_index("year", append=True)
+                      sale
+        month  year
+        1      2012    55
+        4      2014    40
+        7      2013    84
+        10     2014    31
+
+        >>> df.set_index("year", append=False)
+               sale
+        year
+        2012    55
+        2014    40
+        2013    84
+        2014    31
         """
         inplace = validate_bool_kwarg(inplace, "inplace")
         self._check_inplace_and_allows_duplicate_labels(inplace)
diff --git a/pandas/io/common.py b/pandas/io/common.py
index e0076eb486..1a9e6b4724 100644
--- a/pandas/io/common.py
+++ b/pandas/io/common.py
@@ -71,7 +71,7 @@ from pandas.core.shared_docs import _shared_docs
 
 _VALID_URLS = set(uses_relative + uses_netloc + uses_params)
 _VALID_URLS.discard("")
-_RFC_3986_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9+\-+.]*://")
+_FSSPEC_URL_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9+\-+.]*(::[A-Za-z0-9+\-+.]+)*://")
 
 BaseBufferT = TypeVar("BaseBufferT", bound=BaseBuffer)
 
@@ -291,7 +291,7 @@ def is_fsspec_url(url: FilePath | BaseBuffer) -> bool:
     """
     return (
         isinstance(url, str)
-        and bool(_RFC_3986_PATTERN.match(url))
+        and bool(_FSSPEC_URL_PATTERN.match(url))
         and not url.startswith(("http://", "https://"))
     )
 
diff --git a/pandas/io/json/_normalize.py b/pandas/io/json/_normalize.py
index 45c8876dbe..642408b35b 100644
--- a/pandas/io/json/_normalize.py
+++ b/pandas/io/json/_normalize.py
@@ -147,7 +147,7 @@ def nested_to_record(
     return new_ds
 
 
-def _normalise_json(
+def _normalize_json(
     data: Any,
     key_string: str,
     normalized_dict: dict[str, Any],
@@ -177,7 +177,7 @@ def _normalise_json(
             if not key_string:
                 new_key = new_key.removeprefix(separator)
 
-            _normalise_json(
+            _normalize_json(
                 data=value,
                 key_string=new_key,
                 normalized_dict=normalized_dict,
@@ -188,7 +188,7 @@ def _normalise_json(
     return normalized_dict
 
 
-def _normalise_json_ordered(data: dict[str, Any], separator: str) -> dict[str, Any]:
+def _normalize_json_ordered(data: dict[str, Any], separator: str) -> dict[str, Any]:
     """
     Order the top level keys and then recursively go to depth
 
@@ -201,10 +201,10 @@ def _normalise_json_ordered(data: dict[str, Any], separator: str) -> dict[str, A
 
     Returns
     -------
-    dict or list of dicts, matching `normalised_json_object`
+    dict or list of dicts, matching `normalized_json_object`
     """
     top_dict_ = {k: v for k, v in data.items() if not isinstance(v, dict)}
-    nested_dict_ = _normalise_json(
+    nested_dict_ = _normalize_json(
         data={k: v for k, v in data.items() if isinstance(v, dict)},
         key_string="",
         normalized_dict={},
@@ -235,7 +235,7 @@ def _simple_json_normalize(
     Returns
     -------
     frame : DataFrame
-    d - dict or list of dicts, matching `normalised_json_object`
+    d - dict or list of dicts, matching `normalized_json_object`
 
     Examples
     --------
@@ -256,14 +256,14 @@ def _simple_json_normalize(
 }
 
     """
-    normalised_json_object = {}
+    normalized_json_object = {}
     # expect a dictionary, as most jsons are. However, lists are perfectly valid
     if isinstance(ds, dict):
-        normalised_json_object = _normalise_json_ordered(data=ds, separator=sep)
+        normalized_json_object = _normalize_json_ordered(data=ds, separator=sep)
     elif isinstance(ds, list):
-        normalised_json_list = [_simple_json_normalize(row, sep=sep) for row in ds]
-        return normalised_json_list
-    return normalised_json_object
+        normalized_json_list = [_simple_json_normalize(row, sep=sep) for row in ds]
+        return normalized_json_list
+    return normalized_json_object
 
 
 def json_normalize(
diff --git a/requirements-dev.txt b/requirements-dev.txt
index fb4d9cdb58..990901958c 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -85,6 +85,8 @@ feedparser
 pyyaml
 requests
 pygments
+jupyterlite-core
+jupyterlite-pyodide-kernel
 adbc-driver-postgresql>=0.10.0
 adbc-driver-sqlite>=0.8.0
 typing_extensions; python_version<"3.11"
diff --git a/web/interactive_terminal/README.md b/web/interactive_terminal/README.md
new file mode 100644
index 0000000000..6457cbccf2
--- /dev/null
+++ b/web/interactive_terminal/README.md
@@ -0,0 +1,38 @@
+# The interactive `pandas` REPL
+
+An interactive REPL to easily try `pandas` in the browser, powered by JupyterLite.
+
+![image](https://user-images.githubusercontent.com/591645/175000291-e8c69f6f-5f2c-48d7-817c-cff05ab2cde9.png)
+
+## Build
+
+The interactive REPL is built with the `jupyter lite` CLI.
+
+First make sure `jupyterlite` and a kernel are installed:
+
+```bash
+python -m pip install jupyterlite-core
+python -m pip install jupyterlite-pyodide-kernel
+```
+
+Then in `web/interactive_terminal`, run the following command:
+
+```bash
+jupyter lite build
+```
+
+## Configuration
+
+This folder contains configuration files for the interactive terminal powered by JupyterLite:
+
+- `jupyter_lite_config.json`: build time configuration, used when building the assets with the `jupyter lite build` command
+- `jupyter-lite.json` run time configuration applied when launching the application in the browser
+
+This interactive `pandas` JupyterLite deployment enables a couple of optimizations to only include the `repl` app in the generated static assets, and disables source maps, which can make the assets smaller and faster to load, at the cost of
+debugging capabilities.
+
+To learn more about it, check out the JupyterLite documentation:
+
+- Optimizations: https://jupyterlite.readthedocs.io/en/latest/howto/configure/advanced/optimizations.html
+- JupyterLite schema: https://jupyterlite.readthedocs.io/en/latest/reference/schema-v0.html
+- CLI reference: https://jupyterlite.readthedocs.io/en/latest/reference/cli.html
diff --git a/web/interactive_terminal/jupyter-lite.json b/web/interactive_terminal/jupyter-lite.json
new file mode 100644
index 0000000000..2199acf1d3
--- /dev/null
+++ b/web/interactive_terminal/jupyter-lite.json
@@ -0,0 +1,10 @@
+{
+  "jupyter-lite-schema-version": 0,
+  "jupyter-config-data": {
+    "appName": "Pandas REPL",
+    "appUrl": "./repl",
+    "enableMemoryStorage": true,
+    "settingsStorageDrivers": ["memoryStorageDriver"],
+    "contentsStorageDrivers": ["memoryStorageDriver"]
+  }
+}
diff --git a/web/interactive_terminal/jupyter_lite_config.json b/web/interactive_terminal/jupyter_lite_config.json
new file mode 100644
index 0000000000..42e64f26f2
--- /dev/null
+++ b/web/interactive_terminal/jupyter_lite_config.json
@@ -0,0 +1,8 @@
+{
+  "LiteBuildConfig": {
+    "apps": ["repl"],
+    "no_unused_shared_packages": true,
+    "output_dir": "../build/lite",
+    "no_sourcemaps": true
+  }
+}
diff --git a/web/pandas/community/ecosystem.md b/web/pandas/community/ecosystem.md
index 2ad8d6243d..dc663654be 100644
--- a/web/pandas/community/ecosystem.md
+++ b/web/pandas/community/ecosystem.md
@@ -590,7 +590,7 @@ df = pd.read_csv("big.csv")  # use all your cores!
 ### [Pandarallel](https://github.com/nalepae/pandarallel)
 
 Pandarallel provides a simple way to parallelize your pandas operations on all your CPUs by changing only one line of code.
-If also displays progress bars.
+It also displays progress bars.
 
 ```python
 from pandarallel import pandarallel
diff --git a/web/pandas/getting_started.md b/web/pandas/getting_started.md
index 801081a9ef..c556eda57a 100644
--- a/web/pandas/getting_started.md
+++ b/web/pandas/getting_started.md
@@ -22,11 +22,23 @@ by [Wes McKinney](https://wesmckinney.com/), creator of pandas.
 
 ## Videos
 
-<iframe width="560" height="315" frameborder="0"
-src="https://www.youtube.com/embed/_T8LGqJtuGc"
-allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
-allowfullscreen></iframe>
+<iframe
+  src="https://www.youtube.com/embed/_T8LGqJtuGc"
+  style="width: 100%; max-width: 560px; height: 315px;"
+  frameborder="0"
+  allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
+  allowfullscreen
+></iframe>
 
 ## Cheat sheet
 
 [pandas cheat sheet](https://pandas.pydata.org/Pandas_Cheat_Sheet.pdf)
+
+## Try pandas in your browser (experimental)
+
+You can try pandas in your browser with the following interactive shell
+without needing to install anything on your system.
+
+<p>
+    <a class="btn btn-primary" href="{{ base_url }}try.html">Try it in your browser</a>
+</p>
diff --git a/web/pandas/index.html b/web/pandas/index.html
index e8aab9e111..4df4d73fb6 100644
--- a/web/pandas/index.html
+++ b/web/pandas/index.html
@@ -18,9 +18,9 @@
                     <div class="col-md-4">
                         <h5>Getting started</h5>
                         <ul>
-                            <!-- <li><a href="{{ base_url }}/try.html">Try pandas online</a></li> -->
                             <li><a href="{{ base_url }}getting_started.html">Install pandas</a></li>
                             <li><a href="{{ base_url }}docs/getting_started/index.html">Getting started</a></li>
+                            <li><a href="{{ base_url }}try.html">Try pandas online</a></li>
                         </ul>
                     </div>
                     <div class="col-md-4">
diff --git a/web/pandas/try.md b/web/pandas/try.md
new file mode 100644
index 0000000000..ee2f98b05a
--- /dev/null
+++ b/web/pandas/try.md
@@ -0,0 +1,12 @@
+# Try pandas in your browser (experimental)
+
+Try our experimental [JupyterLite](https://jupyterlite.readthedocs.io/en/stable/) live shell with `pandas`, powered by [Pyodide](https://pyodide.org/en/stable/).
+
+**Please note it can take a while (>30 seconds) before the shell is initialized and ready to run commands.**
+
+**Running it requires a reasonable amount of bandwidth and resources (>70 MiB on the first load), so it may not work properly on all devices or networks.**
+
+<iframe
+  src="./lite/repl/index.html?toolbar=1&kernel=python&execute=0&code=import%20pandas%20as%20pd%0Adf%20%3D%20pd.DataFrame%28%7B%22num_legs%22%3A%20%5B2%2C%204%5D%2C%20%22num_wings%22%3A%20%5B2%2C%200%5D%7D%2C%20index%3D%5B%22falcon%22%2C%20%22dog%22%5D%29%0Adf"
+  style="width: 100%; max-width: 650px; height: 600px; border: 1px solid #130753;"
+></iframe>


Test patch:
diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py
index e3f49d04a0..87505b1b22 100644
--- a/pandas/tests/arrays/test_datetimes.py
+++ b/pandas/tests/arrays/test_datetimes.py
@@ -769,8 +769,10 @@ class TestDatetimeArray:
     @pytest.mark.parametrize("freq_depr", ["2MIN", "2nS", "2Us"])
     def test_date_range_uppercase_frequency_deprecated(self, freq_depr):
         # GH#9586, GH#54939
-        depr_msg = f"'{freq_depr[1:]}' is deprecated and will be removed in a "
-        f"future version. Please use '{freq_depr.lower()[1:]}' instead."
+        depr_msg = (
+            f"'{freq_depr[1:]}' is deprecated and will be removed in a "
+            f"future version, please use '{freq_depr.lower()[1:]}' instead."
+        )
 
         expected = pd.date_range("1/1/2000", periods=4, freq=freq_depr.lower())
         with tm.assert_produces_warning(FutureWarning, match=depr_msg):
diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py
index 144b361662..e64fab21b8 100644
--- a/pandas/tests/io/json/test_pandas.py
+++ b/pandas/tests/io/json/test_pandas.py
@@ -1753,6 +1753,7 @@ class TestPandasContainer:
         [
             "s3://example-fsspec/",
             "gcs://another-fsspec/file.json",
+            "filecache::s3://yet-another-fsspec/file.json",
             "https://example-site.com/data",
             "some-protocol://data.txt",
         ],
diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py
index e162815271..99af421d5a 100644
--- a/pandas/tests/io/test_common.py
+++ b/pandas/tests/io/test_common.py
@@ -501,6 +501,18 @@ def test_is_fsspec_url():
     assert icom.is_fsspec_url("RFC-3986+compliant.spec://something")
 
 
+def test_is_fsspec_url_chained():
+    # GH#48978 Support chained fsspec URLs
+    # See https://filesystem-spec.readthedocs.io/en/latest/features.html#url-chaining.
+    assert icom.is_fsspec_url("filecache::s3://pandas/test.csv")
+    assert icom.is_fsspec_url("zip://test.csv::filecache::gcs://bucket/file.zip")
+    assert icom.is_fsspec_url("filecache::zip://test.csv::gcs://bucket/file.zip")
+    assert icom.is_fsspec_url("filecache::dask::s3://pandas/test.csv")
+    assert not icom.is_fsspec_url("filecache:s3://pandas/test.csv")
+    assert not icom.is_fsspec_url("filecache:::s3://pandas/test.csv")
+    assert not icom.is_fsspec_url("filecache::://pandas/test.csv")
+
+
 @pytest.mark.parametrize("encoding", [None, "utf-8"])
 @pytest.mark.parametrize("format", ["csv", "json"])
 def test_codecs_encoding(encoding, format):
diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py
index 56a8e4c439..78f39b649c 100644
--- a/pandas/tests/io/test_parquet.py
+++ b/pandas/tests/io/test_parquet.py
@@ -18,6 +18,7 @@ from pandas.compat.pyarrow import (
     pa_version_under15p0,
     pa_version_under17p0,
     pa_version_under19p0,
+    pa_version_under20p0,
 )
 
 import pandas as pd
@@ -1075,27 +1076,34 @@ class TestParquetPyArrow(Base):
             expected=expected,
         )
 
-    @pytest.mark.xfail(
-        pa_version_under17p0, reason="pa.pandas_compat passes 'datetime64' to .astype"
+    @pytest.mark.parametrize(
+        "columns",
+        [
+            [0, 1],
+            pytest.param(
+                [b"foo", b"bar"],
+                marks=pytest.mark.xfail(
+                    pa_version_under20p0,
+                    raises=NotImplementedError,
+                    reason="https://github.com/apache/arrow/pull/44171",
+                ),
+            ),
+            pytest.param(
+                [
+                    datetime.datetime(2011, 1, 1, 0, 0),
+                    datetime.datetime(2011, 1, 1, 1, 1),
+                ],
+                marks=pytest.mark.xfail(
+                    pa_version_under17p0,
+                    reason="pa.pandas_compat passes 'datetime64' to .astype",
+                ),
+            ),
+        ],
     )
-    def test_columns_dtypes_not_invalid(self, pa):
+    def test_columns_dtypes_not_invalid(self, pa, columns):
         df = pd.DataFrame({"string": list("abc"), "int": list(range(1, 4))})
 
-        # numeric
-        df.columns = [0, 1]
-        check_round_trip(df, pa)
-
-        # bytes
-        df.columns = [b"foo", b"bar"]
-        with pytest.raises(NotImplementedError, match="|S3"):
-            # Bytes fails on read_parquet
-            check_round_trip(df, pa)
-
-        # python object
-        df.columns = [
-            datetime.datetime(2011, 1, 1, 0, 0),
-            datetime.datetime(2011, 1, 1, 1, 1),
-        ]
+        df.columns = columns
         check_round_trip(df, pa)
 
     def test_empty_columns(self, pa):