Repository: scrapy/scrapy

Issue:
Type error when we try to retrieve the `FEEDS` setting via CLI and it has a `Path` objects as a key
<!--

Thanks for taking an interest in Scrapy!

If you have a question that starts with "How to...", please see the Scrapy Community page: https://scrapy.org/community/.
The GitHub issue tracker's purpose is to deal with bug reports and feature requests for the project itself.

Keep in mind that by filing an issue, you are expected to comply with Scrapy's Code of Conduct, including treating everyone with respect: https://github.com/scrapy/scrapy/blob/master/CODE_OF_CONDUCT.md

The following is a suggested template to structure your issue, you can find more guidelines at https://doc.scrapy.org/en/latest/contributing.html#reporting-bugs

-->

### Description

When a `Path` object is used as a key in the `FEEDS` dictionary and we try to obtain the `FEEDS` setting via CLI `scrapy settings --get FEEDS`, a type error is raised:

```
TypeError: keys must be str, int, float, bool or None, not PosixPath
```

A complete stack trace is attached below under "Actual behavior".

`Path` objects as keys are allowed as documented in [the FEEDS section of the Feed exports chapter](https://docs.scrapy.org/en/master/topics/feed-exports.html#feeds).

### Steps to Reproduce

1. In a freshly generated scrapy project (e.g. via `scrapy project feeds_bug`), prepend the following lines to `settings.py`:
```python
from pathlib import Path

FEEDS = {
    Path("some_path/file.csv"): {
        "format": "csv"
    }
}
```
2. Execute `scrapy settings --get FEEDS`

**Expected behavior:** Some string representation of the `FEEDS` setting gets printed to standard out, maybe like this:
```
{"some_path/file.csv": {"format": "csv"}}
```

**Actual behavior:** A type error is raised:
```
Traceback (most recent call last):
  File "/Users/paul/Projects/feeds-bug/.direnv/python-3.9.7/bin/scrapy", line 8, in <module>
    sys.exit(execute())
  File "/Users/paul/Projects/feeds-bug/.direnv/python-3.9.7/lib/python3.9/site-packages/scrapy/cmdline.py", line 145, in execute
    _run_print_help(parser, _run_command, cmd, args, opts)
  File "/Users/paul/Projects/feeds-bug/.direnv/python-3.9.7/lib/python3.9/site-packages/scrapy/cmdline.py", line 100, in _run_print_help
    func(*a, **kw)
  File "/Users/paul/Projects/feeds-bug/.direnv/python-3.9.7/lib/python3.9/site-packages/scrapy/cmdline.py", line 153, in _run_command
    cmd.run(args, opts)
  File "/Users/paul/Projects/feeds-bug/.direnv/python-3.9.7/lib/python3.9/site-packages/scrapy/commands/settings.py", line 37, in run
    print(json.dumps(s.copy_to_dict()))
  File "/Users/paul/.pyenv/versions/3.9.7/lib/python3.9/json/__init__.py", line 231, in dumps
    return _default_encoder.encode(obj)
  File "/Users/paul/.pyenv/versions/3.9.7/lib/python3.9/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/Users/paul/.pyenv/versions/3.9.7/lib/python3.9/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
TypeError: keys must be str, int, float, bool or None, not PosixPath
```

**Reproduces how often:** 100%

### Versions

```
Scrapy       : 2.5.1
lxml         : 4.7.1.0
libxml2      : 2.9.12
cssselect    : 1.1.0
parsel       : 1.6.0
w3lib        : 1.22.0
Twisted      : 21.7.0
Python       : 3.9.7 (default, Dec 11 2021, 11:25:57) - [Clang 13.0.0 (clang-1300.0.29.3)]
pyOpenSSL    : 22.0.0 (OpenSSL 1.1.1m  14 Dec 2021)
cryptography : 36.0.1
Platform     : macOS-11.6-x86_64-i386-64bit
```


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/scrapy/scrapy/pull/5384?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) Report
> Merging [#5384](https://codecov.io/gh/scrapy/scrapy/pull/5384?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) (c286bec) into [master](https://codecov.io/gh/scrapy/scrapy/commit/30d5779ea94ed1e9343a4590895a3f5e65e444b9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) (30d5779) will **decrease** coverage by `4.17%`.
> The diff coverage is `100.00%`.


```diff
@@            Coverage Diff             @@
##           master    #5384      +/-   ##
==========================================
- Coverage   88.59%   84.42%   -4.18%     
==========================================
  Files         163      163              
  Lines       10649    10651       +2     
  Branches     1816     1816              
==========================================
- Hits         9435     8992     -443     
- Misses        938     1397     +459     
+ Partials      276      262      -14     
```

| [Impacted Files](https://codecov.io/gh/scrapy/scrapy/pull/5384?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) | Coverage Δ | |
|---|---|---|
| [scrapy/settings/\_\_init\_\_.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L3NldHRpbmdzL19faW5pdF9fLnB5) | `93.06% <100.00%> (+0.08%)` | :arrow_up: |
| [scrapy/core/http2/stream.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvaHR0cDIvc3RyZWFtLnB5) | `27.01% <0.00%> (-64.37%)` | :arrow_down: |
| [scrapy/pipelines/images.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L3BpcGVsaW5lcy9pbWFnZXMucHk=) | `27.82% <0.00%> (-62.61%)` | :arrow_down: |
| [scrapy/core/http2/agent.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvaHR0cDIvYWdlbnQucHk=) | `36.14% <0.00%> (-60.25%)` | :arrow_down: |
| [scrapy/core/downloader/handlers/http2.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvZG93bmxvYWRlci9oYW5kbGVycy9odHRwMi5weQ==) | `43.42% <0.00%> (-56.58%)` | :arrow_down: |
| [scrapy/core/http2/protocol.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvaHR0cDIvcHJvdG9jb2wucHk=) | `34.17% <0.00%> (-49.25%)` | :arrow_down: |
| [scrapy/robotstxt.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L3JvYm90c3R4dC5weQ==) | `75.30% <0.00%> (-22.23%)` | :arrow_down: |
| [scrapy/utils/ssl.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L3V0aWxzL3NzbC5weQ==) | `53.65% <0.00%> (-17.08%)` | :arrow_down: |
| [scrapy/core/downloader/contextfactory.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L2NvcmUvZG93bmxvYWRlci9jb250ZXh0ZmFjdG9yeS5weQ==) | `75.92% <0.00%> (-11.12%)` | :arrow_down: |
| [scrapy/utils/test.py](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-c2NyYXB5L3V0aWxzL3Rlc3QucHk=) | `50.00% <0.00%> (-10.94%)` | :arrow_down: |
| ... and [6 more](https://codecov.io/gh/scrapy/scrapy/pull/5384/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) | |


Patch:
diff --git a/scrapy/settings/__init__.py b/scrapy/settings/__init__.py
index 1fe1e6fd1..6b1ad0828 100644
--- a/scrapy/settings/__init__.py
+++ b/scrapy/settings/__init__.py
@@ -375,9 +375,13 @@ class BaseSettings(MutableMapping):
         return len(self.attributes)
 
     def _to_dict(self):
-        return {k: (v._to_dict() if isinstance(v, BaseSettings) else v)
+        return {self._get_key(k): (v._to_dict() if isinstance(v, BaseSettings) else v)
                 for k, v in self.items()}
 
+    def _get_key(self, key_value):
+        return (key_value if isinstance(key_value, (bool, float, int, str, type(None)))
+                else str(key_value))
+
     def copy_to_dict(self):
         """
         Make a copy of current settings and convert to a dict.


Test patch:
diff --git a/tests/test_cmdline/__init__.py b/tests/test_cmdline/__init__.py
index 591075a98..8233e0101 100644
--- a/tests/test_cmdline/__init__.py
+++ b/tests/test_cmdline/__init__.py
@@ -64,3 +64,7 @@ class CmdlineTest(unittest.TestCase):
         settingsdict = json.loads(settingsstr)
         self.assertCountEqual(settingsdict.keys(), EXTENSIONS.keys())
         self.assertEqual(200, settingsdict[EXT_PATH])
+
+    def test_pathlib_path_as_feeds_key(self):
+        self.assertEqual(self._execute('settings', '--get', 'FEEDS'),
+                         json.dumps({"items.csv": {"format": "csv", "fields": ["price", "name"]}}))
diff --git a/tests/test_cmdline/settings.py b/tests/test_cmdline/settings.py
index 8a719ddf2..b0ac6e98b 100644
--- a/tests/test_cmdline/settings.py
+++ b/tests/test_cmdline/settings.py
@@ -1,5 +1,14 @@
+from pathlib import Path
+
 EXTENSIONS = {
     'tests.test_cmdline.extensions.TestExtension': 0,
 }
 
 TEST1 = 'default'
+
+FEEDS = {
+    Path('items.csv'): {
+        'format': 'csv',
+        'fields': ['price', 'name'],
+    },
+}