nebius/SWE-bench-extra huggingface dataset:
Repository: scrapinghub/price-parser

Issue:
It can't parse Asian text currency like '원',  '円'.
Korean, and Japanese uses each specific money character like '₩', '￥'.
But that country also using their text based money character to represent currency.
Korean uses '원', Japanese uses '円'.
but this parser can't parse that character.

Thank you.

Comments:

Patch:
diff --git a/price_parser/_currencies.py b/price_parser/_currencies.py
index ed0ce46..a6563aa 100644
--- a/price_parser/_currencies.py
+++ b/price_parser/_currencies.py
@@ -648,6 +648,7 @@ CURRENCIES: Dict[str, Dict] = {
         "s": "¥",
         "n": "Japanese Yen",
         "sn": "￥",
+        "sn2": ["円"],
         "d": 0,
         "r": 0,
         "np": "Japanese yen"
@@ -687,7 +688,7 @@ CURRENCIES: Dict[str, Dict] = {
     "KPW": {
         "s": "₩",
         "n": "North Korean Won",
-        "sn": "₩",
+        "sn": "원",
         "d": 0,
         "r": 0,
         "np": "North Korean Won"
@@ -695,7 +696,7 @@ CURRENCIES: Dict[str, Dict] = {
     "KRW": {
         "s": "₩",
         "n": "South Korean Won",
-        "sn": "₩",
+        "sn": "원",
         "d": 0,
         "r": 0,
         "np": "South Korean won"


Test patch:
diff --git a/tests/test_price_parsing.py b/tests/test_price_parsing.py
index e7fd9bf..28ee0a3 100644
--- a/tests/test_price_parsing.py
+++ b/tests/test_price_parsing.py
@@ -82,7 +82,11 @@ PRICE_PARSING_EXAMPLES_NEW = [
     Example(None, 'AED 8000 (USD 2179)',
             'AED', '8000', 8000),
     Example(None, '13800 ₶',
-            '₶', '13800', 13800)
+            '₶', '13800', 13800),
+    Example(None, '12,000원',
+            '원', '12,000', 12000),
+    Example(None, '3,500円',
+            '円', '3,500', 3500)
 ]
Repository: scrapinghub/price-parser

Issue:
Bug non break space.
Some store show price with non-break space and parser return None.

Comments:

Patch:
diff --git a/price_parser/parser.py b/price_parser/parser.py
index d1051e4..94e9e9c 100644
--- a/price_parser/parser.py
+++ b/price_parser/parser.py
@@ -188,9 +188,13 @@ def extract_price_text(price: str) -> Optional[str]:
     >>> extract_price_text("50%")
     >>> extract_price_text("50")
     '50'
+    >>> extract_price_text("$1\xa0298,00")
+    '1 298,00'
     >>> extract_price_text("$.75")
     '.75'
     """
+    price = re.sub(r'\s+', ' ', price)  # clean initial text from non-breaking and extra spaces
+
     if price.count('€') == 1:
         m = re.search(r"""
         [\d\s.,]*?\d    # number, probably with thousand separators


Test patch:
diff --git a/tests/test_price_parsing.py b/tests/test_price_parsing.py
index 8ef2b11..a6b3c52 100644
--- a/tests/test_price_parsing.py
+++ b/tests/test_price_parsing.py
@@ -429,6 +429,12 @@ PRICE_PARSING_EXAMPLES = [
             '€', '11,76', 11.76),
     Example('$99.99', '$99.99',
             '$', '99.99', 99.99),
+    Example('1\xa0298,00 €', '1\xa0298,00 €',
+            '€', '1 298,00', 1298.00),
+    Example('$1\xa0298,00', '$1\xa0298,00',
+            '$', '1 298,00', 1298.00),
+    Example('1\xa0298,00', '1\xa0298,00',
+            None, '1 298,00', 1298.00),
     Example(None, '.75 €',
             '€', '.75', 0.75),
     Example('$.75', '$.75',
Repository: scrapinghub/spidermon

Issue:
It's not possible to specify SPIDERMON_SLACK_RECIPIENTS via Scrapy Cloud
We should replace `settings.get` by `settings.getlist`. I'm working on a pull request. 

Comments:

Patch:
diff --git a/spidermon/contrib/actions/slack/__init__.py b/spidermon/contrib/actions/slack/__init__.py
index 8674c0e..0de8dd4 100644
--- a/spidermon/contrib/actions/slack/__init__.py
+++ b/spidermon/contrib/actions/slack/__init__.py
@@ -194,7 +194,7 @@ class SendSlackMessage(ActionWithTemplates):
         return {
             "sender_token": crawler.settings.get("SPIDERMON_SLACK_SENDER_TOKEN"),
             "sender_name": crawler.settings.get("SPIDERMON_SLACK_SENDER_NAME"),
-            "recipients": crawler.settings.get("SPIDERMON_SLACK_RECIPIENTS"),
+            "recipients": crawler.settings.getlist("SPIDERMON_SLACK_RECIPIENTS"),
             "message": crawler.settings.get("SPIDERMON_SLACK_MESSAGE"),
             "message_template": crawler.settings.get(
                 "SPIDERMON_SLACK_MESSAGE_TEMPLATE"


Test patch:
diff --git a/tests/contrib/actions/slack/test_slack_message_manager.py b/tests/contrib/actions/slack/test_slack_message_manager.py
index 1035957..63ee934 100644
--- a/tests/contrib/actions/slack/test_slack_message_manager.py
+++ b/tests/contrib/actions/slack/test_slack_message_manager.py
@@ -1,5 +1,11 @@
 import pytest
-from spidermon.contrib.actions.slack import SlackMessageManager
+
+from scrapy.utils.test import get_crawler
+
+from spidermon.contrib.actions.slack import (
+    SlackMessageManager,
+    SendSlackMessage,
+)
 
 
 @pytest.fixture
@@ -39,3 +45,13 @@ def test_api_call_with_error_should_log_error_msg(mocker, logger_error):
 
     assert logger_error.call_count == 1
     assert error_msg in logger_error.call_args_list[0][0]
+
+
+@pytest.mark.parametrize("recipients", ["foo,bar", ["foo", "bar"]])
+def test_load_recipients_list_from_crawler_settings(recipients):
+    settings = {
+        "SPIDERMON_SLACK_RECIPIENTS": recipients,
+    }
+    crawler = get_crawler(settings_dict=settings)
+    kwargs = SendSlackMessage.from_crawler_kwargs(crawler)
+    assert kwargs["recipients"] == ["foo", "bar"]
Repository: scrapy/protego

Issue:
Accept robots.txt as bytes
```python
>>> robots_txt = b"User-Agent: *\nDisallow: /\n"
>>> robots_txt_parser = Protego.parse(robots_txt)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/adrian/temporal/venv/lib/python3.9/site-packages/protego.py", line 310, in parse
    o._parse_robotstxt(content)
  File "/home/adrian/temporal/venv/lib/python3.9/site-packages/protego.py", line 327, in _parse_robotstxt
    hash_pos = line.find('#')
TypeError: argument should be integer or bytes-like object, not 'str'
>>> robots_txt = "User-Agent: *\nDisallow: /\n"
>>> robots_txt_parser = Protego.parse(robots_txt)
>>>
```

Comments:

Patch:
diff --git a/src/protego.py b/src/protego.py
index f859e0c..205e5d5 100644
--- a/src/protego.py
+++ b/src/protego.py
@@ -299,6 +299,8 @@ class Protego(object):
     @classmethod
     def parse(cls, content):
         o = cls()
+        if not isinstance(content, str):
+            raise ValueError(f"Protego.parse expects str, got {type(content).__name__}")
         o._parse_robotstxt(content)
         return o
 


Test patch:
diff --git a/tests/test_protego.py b/tests/test_protego.py
index 97ffdfe..6389b02 100644
--- a/tests/test_protego.py
+++ b/tests/test_protego.py
@@ -1059,3 +1059,10 @@ class TestProtego(TestCase):
         rp = Protego.parse(content)
         self.assertFalse(rp.can_fetch("http://example.com/", "FooBot"))
         self.assertFalse(rp.can_fetch("http://example.com", "FooBot"))
+
+    def test_bytestrings(self):
+        content = b"User-Agent: FootBot\nDisallow: /something"
+        with self.assertRaises(ValueError) as context:
+            Protego.parse(content=content)
+
+        self.assertEqual("Protego.parse expects str, got bytes", str(context.exception))
Repository: scrapy/scrapy

Issue:
lots of warnings `RuntimeWarning: Could not load referrer policy ''` when running tests
Check `tox -e py36 -- tests/test_crawl.py -s` output. @redapple is it a testing issue, or a problem with the warning? I've first noticed it in https://travis-ci.org/scrapy/scrapy/jobs/208185821 output.

Comments:

Patch:
diff --git a/scrapy/spidermiddlewares/referer.py b/scrapy/spidermiddlewares/referer.py
index b444e34bb..1ddfb37f4 100644
--- a/scrapy/spidermiddlewares/referer.py
+++ b/scrapy/spidermiddlewares/referer.py
@@ -261,6 +261,9 @@ _policy_classes = {p.name: p for p in (
     DefaultReferrerPolicy,
 )}
 
+# Reference: https://www.w3.org/TR/referrer-policy/#referrer-policy-empty-string
+_policy_classes[''] = NoReferrerWhenDowngradePolicy
+
 
 def _load_policy_class(policy, warning_only=False):
     """
@@ -317,8 +320,9 @@ class RefererMiddleware(object):
         policy_name = request.meta.get('referrer_policy')
         if policy_name is None:
             if isinstance(resp_or_url, Response):
-                policy_name = to_native_str(
-                    resp_or_url.headers.get('Referrer-Policy', '').decode('latin1'))
+                policy_header = resp_or_url.headers.get('Referrer-Policy')
+                if policy_header is not None:
+                    policy_name = to_native_str(policy_header.decode('latin1'))
         if policy_name is None:
             return self.default_policy()
 


Test patch:
diff --git a/tests/test_spidermiddleware_referer.py b/tests/test_spidermiddleware_referer.py
index b1c815876..f27f31b74 100644
--- a/tests/test_spidermiddleware_referer.py
+++ b/tests/test_spidermiddleware_referer.py
@@ -526,6 +526,13 @@ class TestPolicyHeaderPredecence003(MixinNoReferrerWhenDowngrade, TestRefererMid
     settings = {'REFERRER_POLICY': 'scrapy.spidermiddlewares.referer.OriginWhenCrossOriginPolicy'}
     resp_headers = {'Referrer-Policy': POLICY_NO_REFERRER_WHEN_DOWNGRADE.title()}
 
+class TestPolicyHeaderPredecence004(MixinNoReferrerWhenDowngrade, TestRefererMiddleware):
+    """
+    The empty string means "no-referrer-when-downgrade"
+    """
+    settings = {'REFERRER_POLICY': 'scrapy.spidermiddlewares.referer.OriginWhenCrossOriginPolicy'}
+    resp_headers = {'Referrer-Policy': ''}
+
 
 class TestReferrerOnRedirect(TestRefererMiddleware):
Repository: scrapy/scrapy

Issue:
Wrong charset handling in MailSender
Looks like passing `charset='utf-8'` makes a plain text message with `Content-Transfer-Encoding: base64` which then can't be read.

At https://github.com/scrapy/scrapy/blob/5a75b14a5fbbbd37c14aa7317761655ac7706b70/scrapy/mail.py#L81 `set_charset` is called but as the payload is not set yet, the underlying class just sets the headers. When later `set_payload` is called, it doesn't do any encoding, but the `Content-Transfer-Encoding` is already set. Looks like the fix should be passing the encoding to `set_payload` too, like was proposed in #3722 (and `set_charset` may be safe to be removed, not sure about this). Note that we have tests but they don't catch this.

Note also, that all of this seems to be compat code according to the Python docs.

Comments:

Patch:
diff --git a/scrapy/mail.py b/scrapy/mail.py
index c11f3898d..237327451 100644
--- a/scrapy/mail.py
+++ b/scrapy/mail.py
@@ -96,10 +96,9 @@ class MailSender:
             rcpts.extend(cc)
             msg["Cc"] = COMMASPACE.join(cc)
 
-        if charset:
-            msg.set_charset(charset)
-
         if attachs:
+            if charset:
+                msg.set_charset(charset)
             msg.attach(MIMEText(body, "plain", charset or "us-ascii"))
             for attach_name, mimetype, f in attachs:
                 part = MIMEBase(*mimetype.split("/"))
@@ -110,7 +109,7 @@ class MailSender:
                 )
                 msg.attach(part)
         else:
-            msg.set_payload(body)
+            msg.set_payload(body, charset)
 
         if _callback:
             _callback(to=to, subject=subject, body=body, cc=cc, attach=attachs, msg=msg)


Test patch:
diff --git a/tests/test_mail.py b/tests/test_mail.py
index 504c78486..2535e58db 100644
--- a/tests/test_mail.py
+++ b/tests/test_mail.py
@@ -111,7 +111,7 @@ class MailSenderTest(unittest.TestCase):
 
         msg = self.catched_msg["msg"]
         self.assertEqual(msg["subject"], subject)
-        self.assertEqual(msg.get_payload(), body)
+        self.assertEqual(msg.get_payload(decode=True).decode("utf-8"), body)
         self.assertEqual(msg.get_charset(), Charset("utf-8"))
         self.assertEqual(msg.get("Content-Type"), 'text/plain; charset="utf-8"')
Repository: scrapy/scrapyd

Issue:
Replace `FEED_URI` and `FEED_FORMAT` with `FEEDS` in feedexporter
Running Scrapyd + Scrapy 2.5 results in following warning:

```
scrapy/extensions/feedexport.py:247: ScrapyDeprecationWarning: The `FEED_URI` and `FEED_FORMAT` settings have been deprecated in favor of the `FEEDS` setting. Please see the `FEEDS` setting docs for more details
  exporter = cls(crawler)
```

[FEEDS were added in 2.1](https://docs.scrapy.org/en/latest/topics/feed-exports.html#feeds). We should pay attention to this warning. This will break compatibility with Scrapy below 2, so would be recommended to aslo update setup.py with note we require > 2. 

Comments:

Patch:
diff --git a/docs/install.rst b/docs/install.rst
index 7ca72d6..9cc265c 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -13,7 +13,7 @@ Scrapyd depends on the following libraries, but the installation process
 takes care of installing the missing ones:
 
 * Python 3.6 or above
-* Scrapy 1.2 or above
+* Scrapy 2.0 or above
 * Twisted 17.9 or above
 
 Installing Scrapyd (generic way)
diff --git a/setup.py b/setup.py
index 2b1c838..6288079 100644
--- a/setup.py
+++ b/setup.py
@@ -46,7 +46,7 @@ setup_args = {
 if using_setuptools:
     setup_args['install_requires'] = [
         'Twisted>=17.9',
-        'Scrapy>=1.2.0',
+        'Scrapy>=2.0.0',
         'six'
     ]
     setup_args['entry_points'] = {'console_scripts': [


Test patch:
diff --git a/reqs/requirements-latest.txt b/reqs/requirements-latest.txt
index 438463a..8910a08 100644
--- a/reqs/requirements-latest.txt
+++ b/reqs/requirements-latest.txt
@@ -1,2 +1,2 @@
-Scrapy>=1.1
+Scrapy>=2.0
 twisted
Repository: scrapy/w3lib

Issue:
allow to customize encoding in w3lib.http.basic_auth_header 
See https://github.com/scrapy/scrapy/pull/2530

Comments:

Patch:
diff --git a/w3lib/http.py b/w3lib/http.py
index accfb5d..c7b94a2 100644
--- a/w3lib/http.py
+++ b/w3lib/http.py
@@ -78,7 +78,7 @@ def headers_dict_to_raw(headers_dict):
     return b'\r\n'.join(raw_lines)
 
 
-def basic_auth_header(username, password):
+def basic_auth_header(username, password, encoding='ISO-8859-1'):
     """
     Return an `Authorization` header field value for `HTTP Basic Access Authentication (RFC 2617)`_
 
@@ -95,5 +95,5 @@ def basic_auth_header(username, password):
         # XXX: RFC 2617 doesn't define encoding, but ISO-8859-1
         # seems to be the most widely used encoding here. See also:
         # http://greenbytes.de/tech/webdav/draft-ietf-httpauth-basicauth-enc-latest.html
-        auth = auth.encode('ISO-8859-1')
+        auth = auth.encode(encoding)
     return b'Basic ' + urlsafe_b64encode(auth)


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
index 453624f..01f903e 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
 import unittest
 from collections import OrderedDict
 from w3lib.http import (basic_auth_header,
@@ -14,6 +16,13 @@ class HttpTests(unittest.TestCase):
         self.assertEqual(b'Basic c29tZXVzZXI6QDx5dTk-Jm8_UQ==',
             basic_auth_header('someuser', '@<yu9>&o?Q'))
 
+    def test_basic_auth_header_encoding(self):
+        self.assertEqual(b'Basic c29tw6Z1c8Oocjpzw7htZXDDpHNz',
+                basic_auth_header(u'somæusèr', u'sømepäss', encoding='utf8'))
+        # default encoding (ISO-8859-1)
+        self.assertEqual(b'Basic c29t5nVz6HI6c_htZXDkc3M=',
+                basic_auth_header(u'somæusèr', u'sømepäss'))
+
     def test_headers_raw_dict_none(self):
         self.assertIsNone(headers_raw_to_dict(None))
         self.assertIsNone(headers_dict_to_raw(None))
Repository: scrapy/w3lib

Issue:
url_query_cleaner appends ? to urls without a query string
```python
>>> url_query_cleaner('http://domain.tld/', ['bla'], remove=True)
'http://domain.tld/?'
```

This is the code which does it: `url = '?'.join([base, sep.join(querylist)]) if querylist else base` (`querylist==['']`).

Comments:

Patch:
diff --git a/w3lib/url.py b/w3lib/url.py
index 4be74f7..fc9b343 100644
--- a/w3lib/url.py
+++ b/w3lib/url.py
@@ -182,6 +182,8 @@ def url_query_cleaner(url, parameterlist=(), sep='&', kvsep='=', remove=False, u
     seen = set()
     querylist = []
     for ksv in query.split(sep):
+        if not ksv:
+            continue
         k, _, _ = ksv.partition(kvsep)
         if unique and k in seen:
             continue


Test patch:
diff --git a/tests/test_url.py b/tests/test_url.py
index 0df5bfd..9476b30 100644
--- a/tests/test_url.py
+++ b/tests/test_url.py
@@ -284,6 +284,10 @@ class UrlTests(unittest.TestCase):
                          'http://example.com/?version=1&pageurl=test&param2=value2')
 
     def test_url_query_cleaner(self):
+        self.assertEqual('product.html',
+                url_query_cleaner("product.html?"))
+        self.assertEqual('product.html',
+                url_query_cleaner("product.html?&"))
         self.assertEqual('product.html?id=200',
                 url_query_cleaner("product.html?id=200&foo=bar&name=wired", ['id']))
         self.assertEqual('product.html?id=200',
@@ -308,6 +312,10 @@ class UrlTests(unittest.TestCase):
                 url_query_cleaner("product.html?id=2&foo=bar&name=wired", ['id', 'foo'], remove=True))
         self.assertEqual('product.html?foo=bar&name=wired',
                 url_query_cleaner("product.html?id=2&foo=bar&name=wired", ['id', 'footo'], remove=True))
+        self.assertEqual('product.html',
+                url_query_cleaner("product.html", ['id'], remove=True))
+        self.assertEqual('product.html',
+                url_query_cleaner("product.html?&", ['id'], remove=True))
         self.assertEqual('product.html?foo=bar',
                 url_query_cleaner("product.html?foo=bar&name=wired", 'foo'))
         self.assertEqual('product.html?foobar=wired',
Repository: scrapy/w3lib

Issue:
remove_tags_with_content bug
use this 
`remove_tags_with_content("""<div class="hqimg_related"><div class="to_page"><span>热点栏目</span> <s class='hotSe'>
""", ("s",))`
get result:
`<div class="hqimg_related"><div class="to_page">`
There may be some mistakes
then modified the expression. 
`tags = '|'.join([r'<%s\s+.*?</%s>|<%s\s*/>' % (tag, tag, tag) for tag in which_ones])`
finally,it's work fine


Comments:

Patch:
diff --git a/w3lib/html.py b/w3lib/html.py
index 9990a35..55fe5e3 100644
--- a/w3lib/html.py
+++ b/w3lib/html.py
@@ -220,7 +220,7 @@ def remove_tags_with_content(text, which_ones=(), encoding=None):
 
     text = to_unicode(text, encoding)
     if which_ones:
-        tags = '|'.join([r'<%s.*?</%s>|<%s\s*/>' % (tag, tag, tag) for tag in which_ones])
+        tags = '|'.join([r'<%s\b.*?</%s>|<%s\s*/>' % (tag, tag, tag) for tag in which_ones])
         retags = re.compile(tags, re.DOTALL | re.IGNORECASE)
         text = retags.sub(u'', text)
     return text


Test patch:
diff --git a/tests/test_html.py b/tests/test_html.py
index 68133cb..6d6e8ae 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -184,6 +184,10 @@ class RemoveTagsWithContentTest(unittest.TestCase):
         # text with empty tags
         self.assertEqual(remove_tags_with_content(u'<br/>a<br />', which_ones=('br',)), u'a')
 
+    def test_tags_with_shared_prefix(self):
+        # https://github.com/scrapy/w3lib/issues/114
+        self.assertEqual(remove_tags_with_content(u'<span></span>

', which_ones=('s',)), u'<span></span>')
+
 
 class ReplaceEscapeCharsTest(unittest.TestCase):
     def test_returns_unicode(self):
Repository: scrapy/w3lib

Issue:
Function `convert_entity` does not catch `OverflowError` 
Error:
```
OverflowError
Python int too large to convert to C int
```

`w3lib` version -> `2.0.1`
`Python` version -> `3.8.13`

<img width="1131" alt="Screenshot 2022-10-31 at 10 02 22" src="https://user-images.githubusercontent.com/107861298/198971171-1af35f01-42d2-413a-8c47-9d2c3dc6ece1.png">


Comments:

Patch:
diff --git a/w3lib/html.py b/w3lib/html.py
index a31d42b..0cff2ff 100644
--- a/w3lib/html.py
+++ b/w3lib/html.py
@@ -91,7 +91,7 @@ def replace_entities(
                     return bytes((number,)).decode("cp1252")
                 else:
                     return chr(number)
-            except ValueError:
+            except (ValueError, OverflowError):
                 pass
 
         return "" if remove_illegal and groups.get("semicolon") else m.group(0)


Test patch:
diff --git a/tests/test_html.py b/tests/test_html.py
index 1e637b0..1cabf6d 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -65,6 +65,10 @@ class RemoveEntitiesTest(unittest.TestCase):
         self.assertEqual(replace_entities("x&#x2264;y"), "x\u2264y")
         self.assertEqual(replace_entities("x&#157;y"), "xy")
         self.assertEqual(replace_entities("x&#157;y", remove_illegal=False), "x&#157;y")
+        self.assertEqual(replace_entities("&#82179209091;"), "")
+        self.assertEqual(
+            replace_entities("&#82179209091;", remove_illegal=False), "&#82179209091;"
+        )
 
     def test_browser_hack(self):
         # check browser hack for numeric character references in the 80-9F range
Repository: scverse/scanpy

Issue:
String conversion of `qc_vars` into a `Collection` when a `str` input is provided in `calculate_qc_metrics`
### What kind of feature would you like to request?

Additional function parameters / changed functionality / changed defaults?

### Please describe your wishes

I often make the mistake of providing a single string input to the `qc_vars` argument in `calculate_qc_metrics` which always fails with a `KeyError` because it beings to iterate over the string. This could be a very simple addition to just convert the argument into a `list` if a `str` is provided as input. 

Comments:

Patch:
diff --git a/docs/release-notes/1.10.0.md b/docs/release-notes/1.10.0.md
index 7d48d4fd..1bb50c89 100644
--- a/docs/release-notes/1.10.0.md
+++ b/docs/release-notes/1.10.0.md
@@ -18,6 +18,7 @@
 * {func}`scanpy.tl.rank_genes_groups` no longer warns that it's default was changed from t-test_overestim_var to t-test {pr}`2798` {smaller}`L Heumos`
 * {func}`scanpy.pp.highly_variable_genes` has new flavor `seurat_v3_paper` that is in its implementation consistent with the paper description in Stuart et al 2018. {pr}`2792` {smaller}`E Roellin`
 * {func}`scanpy.pp.highly_variable_genes` supports dask for the default `seurat` and `cell_ranger` flavors {pr}`2809` {smaller}`P Angerer`
+* Auto conversion of strings to collections in `scanpy.pp.calculate_qc_metrics` {pr}`2859` {smaller}`N Teyssier`
 
 ```{rubric} Docs
 ```
diff --git a/scanpy/preprocessing/_qc.py b/scanpy/preprocessing/_qc.py
index 241afb32..4d1123f6 100644
--- a/scanpy/preprocessing/_qc.py
+++ b/scanpy/preprocessing/_qc.py
@@ -233,7 +233,7 @@ def calculate_qc_metrics(
     *,
     expr_type: str = "counts",
     var_type: str = "genes",
-    qc_vars: Collection[str] = (),
+    qc_vars: Collection[str] | str = (),
     percent_top: Collection[int] | None = (50, 100, 200, 500),
     layer: str | None = None,
     use_raw: bool = False,
@@ -308,6 +308,10 @@ def calculate_qc_metrics(
     if issparse(X):
         X.eliminate_zeros()
 
+    # Convert qc_vars to list if str
+    if isinstance(qc_vars, str):
+        qc_vars = [qc_vars]
+
     obs_metrics = describe_obs(
         adata,
         expr_type=expr_type,


Test patch:
diff --git a/scanpy/tests/test_qc_metrics.py b/scanpy/tests/test_qc_metrics.py
index 06a4d0ce..83971fa2 100644
--- a/scanpy/tests/test_qc_metrics.py
+++ b/scanpy/tests/test_qc_metrics.py
@@ -143,6 +143,16 @@ def test_qc_metrics_format(cls):
         assert np.allclose(adata.var[col], adata_dense.var[col])
 
 
+def test_qc_metrics_format_str_qc_vars():
+    adata_dense, init_var = adata_mito()
+    sc.pp.calculate_qc_metrics(adata_dense, qc_vars="mito", inplace=True)
+    adata = AnnData(X=adata_dense.X, var=init_var.copy())
+    sc.pp.calculate_qc_metrics(adata, qc_vars="mito", inplace=True)
+    assert np.allclose(adata.obs, adata_dense.obs)
+    for col in adata.var:  # np.allclose doesn't like mix of types
+        assert np.allclose(adata.var[col], adata_dense.var[col])
+
+
 def test_qc_metrics_percentage():  # In response to #421
     adata_dense, init_var = adata_mito()
     sc.pp.calculate_qc_metrics(adata_dense, percent_top=[])