resolved RESOLVED PASS · 37 tool calls · 185 s · Textualize/rich
Fix [BUG] @ breaks highlighting in hyperlink #3327 ## Type of changes - [x] Bug fix - [ ] New feature - [ ] Documentation / docstrings - [x] Tests - [ ] Other ## Checklist - [ ] I've run the latest [black](https://github.com/psf/black) with default args on new code. - [ ] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate. - [x] I've added tests for new code. - [x] I accept that @willmcgugan may be pedantic in the code review. ## Description Fixed the bug [#3327](https://github.com/Textualize/rich/issues/3327). Changes - 1. Added '@' in rich.highlighter:ReprHighlighter url regex pattern. 2. Added a test case in test_highlighter for the url with '@'. ## Note Poetry installed black v22.12.0 and its formatting differs a bit from the latest version v24.8.0. Hence I've formatted with the prior to keep it consistent.
| # | Tool | Arguments | Result |
|---|---|---|---|
| 1 | run_command | {
"command": "grep -rn \"ReprHighlighter\" rich/highlighter.py"
} | {"status": "ok", "stdout": "rich/highlighter.py:80:class ReprHighlighter(RegexHighlighter):\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "rich/highlighter.py:80:class ReprHighlighter(RegexHighlighter):\n", "stderr": "", "exit_code": 0} |
| 2 | run_command | {
"command": "sed -n '80,150p' rich/highlighter.py"
} | {"status": "ok", "stdout": "class ReprHighlighter(RegexHighlighter):\n \"\"\"Highlights the text typically produced from ``__repr__`` methods.\"\"\"\n\n base_stfull{"status": "ok", "stdout": "class ReprHighlighter(RegexHighlighter):\n \"\"\"Highlights the text typically produced from ``__repr__`` methods.\"\"\"\n\n base_style = \"repr.\"\n highlights = [\n r\"(?P<tag_start><)(?P<tag_name>[-\\w.:|]*)(?P<tag_contents>[\\w\\W]*)(?P<tag_end>>)\",\n r'(?P<attrib_name>[\\w_]{1,50})=(?P<attrib_value>\"?[\\w_]+\"?)?',\n r\"(?P<brace>[][{}()])\",\n _combine_regex(\n r\"(?P<ipv4>[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})\",\n r\"(?P<ipv6>([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})\",\n r\"(?P<eui64>(?:[0-9A-Fa-f]{1,2}-){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\\.){3}[0-9A-Fa-f]{4})\",\n r\"(?P<eui48>(?:[0-9A-Fa-f]{1,2}-){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\\.){2}[0-9A-Fa-f]{4})\",\n r\"(?P<uuid>[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\",\n r\"(?P<call>[\\w.]*?)\\(\",\n r\"\\b(?P<bool_true>True)\\b|\\b(?P<bool_false>False)\\b|\\b(?P<none>None)\\b\",\n r\"(?P<ellipsis>\\.\\.\\.)\",\n r\"(?P<number_complex>(?<!\\w)(?:\\-?[0-9]+\\.?[0-9]*(?:e[-+]?\\d+?)?)(?:[-+](?:[0-9]+\\.?[0-9]*(?:e[-+]?\\d+)?))?j)\",\n r\"(?P<number>(?<!\\w)\\-?[0-9]+\\.?[0-9]*(e[-+]?\\d+?)?\\b|0x[0-9a-fA-F]*)\",\n r\"(?P<path>\\B(/[-\\w._+]+)*\\/)(?P<filename>[-\\w._+]*)?\",\n r\"(?<![\\\\\\w])(?P<str>b?'''.*?(?<!\\\\)'''|b?'.*?(?<!\\\\)'|b?\\\"\\\"\\\".*?(?<!\\\\)\\\"\\\"\\\"|b?\\\".*?(?<!\\\\)\\\")\",\n r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\",\n ),\n ]\n\n\nclass JSONHighlighter(RegexHighlighter):\n \"\"\"Highlights JSON\"\"\"\n\n # Captures the start and end of JSON strings, handling escaped quotes\n JSON_STR = r\"(?<![\\\\\\w])(?P<str>b?\\\".*?(?<!\\\\)\\\")\"\n JSON_WHITESPACE = {\" \", \"\\n\", \"\\r\", \"\\t\"}\ |
| 3 | run_command | {
"command": "grep -rn \"test_highlighter\" tests/"
} | {"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}}full{"status": "error", "error_type": "CommandError", "error_message": "", "details": {"stdout": "", "stderr": "", "exit_code": 1}} |
| 4 | run_command | {
"command": "find tests -name \"*highlighter*\""
} | {"status": "ok", "stdout": "tests/test_highlighter.py\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "tests/test_highlighter.py\n", "stderr": "", "exit_code": 0} |
| 5 | run_command | {
"command": "grep -n \"ReprHighlighter\" tests/test_highlighter.py"
} | {"status": "ok", "stdout": "11: ReprHighlighter,\n161: \"\"\"Tests for the regular expressions used in ReprHighlighter.\"\"\"\n163: highlighter = ReprHighlightefull{"status": "ok", "stdout": "11: ReprHighlighter,\n161: \"\"\"Tests for the regular expressions used in ReprHighlighter.\"\"\"\n163: highlighter = ReprHighlighter()\n", "stderr": "", "exit_code": 0} |
| 6 | run_command | {
"command": "sed -n '160,200p' tests/test_highlighter.py"
} | {"status": "ok", "stdout": "def test_highlight_regex(test: str, spans: List[Span]):\n \"\"\"Tests for the regular expressions used in ReprHighlighter.\"\"\"\n tfull{"status": "ok", "stdout": "def test_highlight_regex(test: str, spans: List[Span]):\n \"\"\"Tests for the regular expressions used in ReprHighlighter.\"\"\"\n text = Text(test)\n highlighter = ReprHighlighter()\n highlighter.highlight(text)\n print(text.spans)\n assert text.spans == spans\n\n\ndef test_highlight_json_with_indent():\n json_string = json.dumps({\"name\": \"apple\", \"count\": 1}, indent=4)\n text = Text(json_string)\n highlighter = JSONHighlighter()\n highlighter.highlight(text)\n assert text.spans == [\n Span(0, 1, \"json.brace\"),\n Span(6, 12, \"json.str\"),\n Span(14, 21, \"json.str\"),\n Span(27, 34, \"json.str\"),\n Span(36, 37, \"json.number\"),\n Span(38, 39, \"json.brace\"),\n Span(6, 12, \"json.key\"),\n Span(27, 34, \"json.key\"),\n ]\n\n\ndef test_highlight_json_string_only():\n json_string = '\"abc\"'\n text = Text(json_string)\n highlighter = JSONHighlighter()\n highlighter.highlight(text)\n assert text.spans == [Span(0, 5, \"json.str\")]\n\n\ndef test_highlight_json_empty_string_only():\n json_string = '\"\"'\n text = Text(json_string)\n highlighter = JSONHighlighter()\n highlighter.highlight(text)\n assert text.spans == [Span(0, 2, \"json.str\")]\n\n", "stderr": "", "exit_code": 0} |
| 7 | run_command | {
"command": "grep -n \"test_highlight_regex\" tests/test_highlighter.py -B 100"
} | {"status": "ok", "stdout": "60- ),\n61- (\"foo=bar\", [Span(0, 3, \"repr.attrib_name\"), Span(4, 7, \"repr.attrib_value\")]),\n62- (\n63- 'foo=\"bar\"',\n64- [\full{"status": "ok", "stdout": "60- ),\n61- (\"foo=bar\", [Span(0, 3, \"repr.attrib_name\"), Span(4, 7, \"repr.attrib_value\")]),\n62- (\n63- 'foo=\"bar\"',\n64- [\n65- Span(0, 3, \"repr.attrib_name\"),\n66- Span(4, 9, \"repr.attrib_value\"),\n67- Span(4, 9, \"repr.str\"),\n68- ],\n69- ),\n70- (\n71- \"<Permission.WRITE|READ: 3>\",\n72- [\n73- Span(0, 1, \"repr.tag_start\"),\n74- Span(1, 23, \"repr.tag_name\"),\n75- Span(23, 25, \"repr.tag_contents\"),\n76- Span(25, 26, \"repr.tag_end\"),\n77- Span(24, 25, \"repr.number\"),\n78- ],\n79- ),\n80- (\"( )\", [Span(0, 1, \"repr.brace\"), Span(2, 3, \"repr.brace\")]),\n81- (\"[ ]\", [Span(0, 1, \"repr.brace\"), Span(2, 3, \"repr.brace\")]),\n82- (\"{ }\", [Span(0, 1, \"repr.brace\"), Span(2, 3, \"repr.brace\")]),\n83- (\" 1 \", [Span(1, 2, \"repr.number\")]),\n84- (\" 1.2 \", [Span(1, 4, \"repr.number\")]),\n85- (\" 0xff \", [Span(1, 5, \"repr.number\")]),\n86- (\" 1e10 \", [Span(1, 5, \"repr.number\")]),\n87- (\" 1j \", [Span(1, 3, \"repr.number_complex\")]),\n88- (\" 3.14j \", [Span(1, 6, \"repr.number_complex\")]),\n89- (\n90- \" (3.14+2.06j) \",\n91- [\n92- Span(1, 2, \"repr.brace\"),\n93- Span(12, 13, \"repr.brace\"),\n94- Span(2, 12, \"repr.number_complex\"),\n95- ],\n96- ),\n97- (\n98- \" (3+2j) \",\n99- [\n100- Span(1, 2, \"repr.brace\"),\n101- Span(6, 7, \"repr.brace\"),\n102- Span(2, 6, \"repr.number_complex\"),\n103- ],\n104- ),\n105- (\n106- \" (123456.4321-1234.5678j) \",\n107- [\n108- Span(1, 2, \"repr.brace\"),\n109- Span(24, 25, \"repr.brace\"),\n110- Span(2, 24, \"repr.number_complex\"),\n111- ],\n112- ),\n113- (\n114- \" (-123123-2.1312342342423422e+ |
| 8 | run_command | {
"command": "sed -n '130,160p' tests/test_highlighter.py"
} | {"status": "ok", "stdout": " (\"ED-ED-ED-ED-ED-ED\", [Span(0, 17, \"repr.eui48\")]), # uppercase\n (\"Ed-Ed-Ed-Ed-Ed-Ed\", [Span(0, 17, \"repr.eui48\")]), # mixfull{"status": "ok", "stdout": " (\"ED-ED-ED-ED-ED-ED\", [Span(0, 17, \"repr.eui48\")]), # uppercase\n (\"Ed-Ed-Ed-Ed-Ed-Ed\", [Span(0, 17, \"repr.eui48\")]), # mixed case\n (\"0-00-1-01-2-02\", [Span(0, 14, \"repr.eui48\")]), # dropped zero\n (\" https://example.org \", [Span(1, 20, \"repr.url\")]),\n (\" http://example.org \", [Span(1, 19, \"repr.url\")]),\n (\" http://example.org/index.html \", [Span(1, 30, \"repr.url\")]),\n (\" http://example.org/index.html#anchor \", [Span(1, 37, \"repr.url\")]),\n (\n \" http://example.org/index.html?param1=value1 \",\n [\n Span(31, 37, \"repr.attrib_name\"),\n Span(38, 44, \"repr.attrib_value\"),\n Span(1, 44, \"repr.url\"),\n ],\n ),\n (\" http://example.org/~folder \", [Span(1, 27, \"repr.url\")]),\n (\"No place like 127.0.0.1\", [Span(14, 23, \"repr.ipv4\")]),\n (\"''\", [Span(0, 2, \"repr.str\")]),\n (\"'hello'\", [Span(0, 7, \"repr.str\")]),\n (\"'''hello'''\", [Span(0, 11, \"repr.str\")]),\n ('\"\"', [Span(0, 2, \"repr.str\")]),\n ('\"hello\"', [Span(0, 7, \"repr.str\")]),\n ('\"\"\"hello\"\"\"', [Span(0, 11, \"repr.str\")]),\n (\"\\\\'foo'\", []),\n (\"it's no 'string'\", [Span(8, 16, \"repr.str\")]),\n (\"78351748-9b32-4e08-ad3e-7e9ff124d541\", [Span(0, 36, \"repr.uuid\")]),\n]\n\n\n@pytest.mark.parametrize(\"test, spans\", highlight_tests)\ndef test_highlight_regex(test: str, spans: List[Span]):\n", "stderr": "", "exit_code": 0} |
| 9 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\ntext = Text(\"https://user:pass@example.com\")\nhighlighter = ReprHighlighter()\nhighlighter.highlight(text)\nprint(f\"Spans: {text.spans}\")\nPY\n"
} | {"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\n", "stderr": "", "exit_code": 0} |
| 10 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\n# Test with @ in the middle of the URL\ntexts = [\n \"https://user:pass@example.com\",\n \"http://user@example.com\",\n \"https://example.com/@user\",\n]\n\nfor t_str in texts:\n text = Text(t_str)\n highlighter = ReprHighlighter()\n highlighter.highlight(text)\n print(f\"Text: {t_str} -> Spans: {text.spans}\")\nPY\n"
} | {"status": "ok", "stdout": "Text: https://user:pass@example.com -> Spans: [Span(0, 17, 'repr.url')]\nText: http://user@example.com -> Spans: [Span(0, 11, 'repr.full{"status": "ok", "stdout": "Text: https://user:pass@example.com -> Spans: [Span(0, 17, 'repr.url')]\nText: http://user@example.com -> Spans: [Span(0, 11, 'repr.url')]\nText: https://example.com/@user -> Spans: [Span(0, 20, 'repr.url')]\n", "stderr": "", "exit_code": 0} |
| 11 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\n# The current regex is: r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\"\n# Let's see if it actually fails for some @ cases.\n# Wait, the previous test showed it worked. Let me re-examine the regex.\n# [-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*\n# It does NOT contain '@'.\n# Why did my previous test work?\n# Ah, I might have been mistaken about the output or the regex.\n# Let's check the regex again.\nPY\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 12 | run_command | {
"command": "python3 - <<'PY'\nimport re\nurl_regex = r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\"\ntest_url = \"https://user:pass@example.com\"\nmatch = re.search(url_regex, test_url)\nif match:\n print(f\"Match: {match.group('url')}\")\nelse:\n print(\"No match\")\nPY\n"
} | {"status": "ok", "stdout": "Match: https://user:pass\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Match: https://user:pass\n", "stderr": "", "exit_code": 0} |
| 13 | run_command | {
"command": "python3 - <<'PY'\nimport re\nurl_regex = r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\"\ntest_urls = [\n \"https://user:pass@example.com\",\n \"http://user@example.com\",\n \"https://example.com/@user\",\n]\n\nfor url in test_urls:\n match = re.search(url_regex, url)\n if match:\n print(f\"URL: {url} -> Match: {match.group('url')}\")\n else:\n print(f\"URL: {url} -> No match\")\nPY\n"
} | {"status": "ok", "stdout": "URL: https://user:pass@example.com -> Match: https://user:pass\nURL: http://user@example.com -> Match: http://user\nURL: https://exafull{"status": "ok", "stdout": "URL: https://user:pass@example.com -> Match: https://user:pass\nURL: http://user@example.com -> Match: http://user\nURL: https://example.com/@user -> Match: https://example.com/\n", "stderr": "", "exit_code": 0} |
| 14 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\ntext = Text(\"https://user:pass@example.com\")\nhighlighter = ReprHighlighter()\nhighlighter.highlight(text)\nprint(f\"Spans: {text.spans}\")\nPY\n"
} | {"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\n", "stderr": "", "exit_code": 0} |
| 15 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\ntext = Text(\"https://user:pass@example.com\")\nhighlighter = ReprHighlighter()\nhighlighter.highlight(text)\nprint(f\"Spans: {text.spans}\")\n# Let's check the actual text content of the span\nfor span in text.spans:\n print(f\"Span: {span}, Text: {text.plain[span.start:span.end]}\")\nPY\n"
} | {"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\nSpan: Span(0, 17, 'repr.url'), Text: https://user:pass\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\nSpan: Span(0, 17, 'repr.url'), Text: https://user:pass\n", "stderr": "", "exit_code": 0} |
| 16 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\ntext = Text(\"https://user:pass@example.com\")\nhighlighter = ReprHighlighter()\nhighlighter.highlight(text)\nprint(f\"Spans: {text.spans}\")\n# Wait, I just saw Span(0, 17, 'repr.url') and the text is \"https://user:pass\"\n# Let's count: h(0)t(1)t(2)p(3)s(4):(5)/(6)/(7)u(8)s(9)e(10)r(11):(12)p(13)a(14)s(15)s(16)\n# That's 17 characters.\n# So it matched up to the '@'.\n# \"https://user:pass@example.com\"\n# 01234567890123456\n# The '@' is at index 17.\n# So the span is [0, 17), which is \"https://user:pass\".\n# The '@example.com' part is NOT highlighted.\nPY\n"
} | {"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\n", "stderr": "", "exit_code": 0} |
| 17 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\ntext = Text(\"https://user:pass@example.com\")\nhighlighter = ReprHighlighter()\nhighlighter.highlight(text)\nprint(f\"Spans: {text.spans}\")\n# Let's check the length of the text\nprint(f\"Text length: {len(text.plain)}\")\nPY\n"
} | {"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\nText length: 29\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\nText length: 29\n", "stderr": "", "exit_code": 0} |
| 18 | run_command | {
"command": "python3 - <<'PY'\nimport re\nurl_regex = r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\"\ntest_url = \"https://user:pass@example.com\"\nmatch = re.search(url_regex, test_url)\nif match:\n print(f\"Match: {match.group('url')}\")\nelse:\n print(\"No match\")\nPY\n"
} | {"status": "ok", "stdout": "Match: https://user:pass\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Match: https://user:pass\n", "stderr": "", "exit_code": 0} |
| 19 | run_command | {
"command": "python3 - <<'PY'\nimport re\nurl_regex = r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\"\ntest_url = \"https://user:pass@example.com\"\n# Let's see what happens if we add @ to the regex\nurl_regex_fixed = r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)\"\nmatch = re.search(url_regex_fixed, test_url)\nif match:\n print(f\"Match: {match.group('url')}\")\nelse:\n print(\"No match\")\nPY\n"
} | {"status": "ok", "stdout": "Match: https://user:pass@example.com\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Match: https://user:pass@example.com\n", "stderr": "", "exit_code": 0} |
| 20 | run_command | {
"command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"rich/highlighter.py\")\ns = p.read_text()\nold = r'r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\"'\nnew = r'r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)\"'\n# The old string in the file is actually:\n# r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\"\n# But it's inside a list of strings.\n# Let's use a more flexible replacement.\nimport re\ns = re.sub(r'r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\"', \n r'r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)\"', s)\np.write_text(s)\nPY\n"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 21 | run_command | {
"command": "python3 -m py_compile rich/highlighter.py"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 22 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\ntext = Text(\"https://user:pass@example.com\")\nhighlighter = ReprHighlighter()\nhighlighter.highlight(text)\nprint(f\"Spans: {text.spans}\")\nPY\n"
} | {"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\n", "stderr": "", "exit_code": 0} |
| 23 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\ntext = Text(\"https://user:pass@example.com\")\nhighlighter = ReprHighlighter()\nhighlighter.highlight(text)\nprint(f\"Spans: {text.spans}\")\n# Let's check the actual text content of the span\nfor span in text.spans:\n print(f\"Span: {span}, Text: {text.plain[span.start:span.end]}\")\nPY\n"
} | {"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\nSpan: Span(0, 17, 'repr.url'), Text: https://user:pass\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Spans: [Span(0, 17, 'repr.url')]\nSpan: Span(0, 17, 'repr.url'), Text: https://user:pass\n", "stderr": "", "exit_code": 0} |
| 24 | run_command | {
"command": "python3 - <<'PY'\nimport re\nurl_regex = r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)\"\ntest_url = \"https://user:pass@example.com\"\nmatch = re.search(url_regex, test_url)\nif match:\n print(f\"Match: {match.group('url')}\")\nelse:\n print(\"No match\")\nPY\n"
} | {"status": "ok", "stdout": "Match: https://user:pass@example.com\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Match: https://user:pass@example.com\n", "stderr": "", "exit_code": 0} |
| 25 | run_command | {
"command": "sed -n '110,120p' rich/highlighter.py"
} | {"status": "ok", "stdout": " JSON_STR = r\"(?<![\\\\\\w])(?P<str>b?\\\".*?(?<!\\\\)\\\")\"\n JSON_WHITESPACE = {\" \", \"\\n\", \"\\r\", \"\\t\"}\n\n base_stylefull{"status": "ok", "stdout": " JSON_STR = r\"(?<![\\\\\\w])(?P<str>b?\\\".*?(?<!\\\\)\\\")\"\n JSON_WHITESPACE = {\" \", \"\\n\", \"\\r\", \"\\t\"}\n\n base_style = \"json.\"\n highlights = [\n _combine_regex(\n r\"(?P<brace>[\\{\\[\\(\\)\\]\\}])\",\n r\"\\b(?P<bool_true>true)\\b|\\b(?P<bool_false>false)\\b|\\b(?P<null>null)\\b\",\n r\"(?P<number>(?<!\\w)\\-?[0-9]+\\.?[0-9]*(e[\\-\\+]?\\d+?)?\\b|0x[0-9a-fA-F]*)\",\n JSON_STR,\n ),\n", "stderr": "", "exit_code": 0} |
| 26 | run_command | {
"command": "sed -n '80,110p' rich/highlighter.py"
} | {"status": "ok", "stdout": "class ReprHighlighter(RegexHighlighter):\n \"\"\"Highlights the text typically produced from ``__repr__`` methods.\"\"\"\n\n base_stfull{"status": "ok", "stdout": "class ReprHighlighter(RegexHighlighter):\n \"\"\"Highlights the text typically produced from ``__repr__`` methods.\"\"\"\n\n base_style = \"repr.\"\n highlights = [\n r\"(?P<tag_start><)(?P<tag_name>[-\\w.:|]*)(?P<tag_contents>[\\w\\W]*)(?P<tag_end>>)\",\n r'(?P<attrib_name>[\\w_]{1,50})=(?P<attrib_value>\"?[\\w_]+\"?)?',\n r\"(?P<brace>[][{}()])\",\n _combine_regex(\n r\"(?P<ipv4>[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})\",\n r\"(?P<ipv6>([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})\",\n r\"(?P<eui64>(?:[0-9A-Fa-f]{1,2}-){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\\.){3}[0-9A-Fa-f]{4})\",\n r\"(?P<eui48>(?:[0-9A-Fa-f]{1,2}-){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\\.){2}[0-9A-Fa-f]{4})\",\n r\"(?P<uuid>[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\",\n r\"(?P<call>[\\w.]*?)\\(\",\n r\"\\b(?P<bool_true>True)\\b|\\b(?P<bool_false>False)\\b|\\b(?P<none>None)\\b\",\n r\"(?P<ellipsis>\\.\\.\\.)\",\n r\"(?P<number_complex>(?<!\\w)(?:\\-?[0-9]+\\.?[0-9]*(?:e[-+]?\\d+?)?)(?:[-+](?:[0-9]+\\.?[0-9]*(?:e[-+]?\\d+)?))?j)\",\n r\"(?P<number>(?<!\\w)\\-?[0-9]+\\.?[0-9]*(e[-+]?\\d+?)?\\b|0x[0-9a-fA-F]*)\",\n r\"(?P<path>\\B(/[-\\w._+]+)*\\/)(?P<filename>[-\\w._+]*)?\",\n r\"(?<![\\\\\\w])(?P<str>b?'''.*?(?<!\\\\)'''|b?'.*?(?<!\\\\)'|b?\\\"\\\"\\\".*?(?<!\\\\)\\\"\\\"\\\"|b?\\\".*?(?<!\\\\)\\\")\",\n r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\",\n ),\n ]\n\n\nclass JSONHighlighter(RegexHighlighter):\n \"\"\"Highlights JSON\"\"\"\n\n # Captures the start and end of JSON strings, handling escaped quotes\n JSON_STR = r\"(?<![\\\\\\w])(?P<str>b?\\\".*?(?<!\\\\)\\\")\"\n", "stderr": "", "exit_code": 0} |
| 27 | run_command | {
"command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"rich/highlighter.py\")\ns = p.read_text()\nprint(f\"Line 108: {s.splitlines()[107]}\")\nPY\n"
} | {"status": "ok", "stdout": "Line 108: \n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Line 108: \n", "stderr": "", "exit_code": 0} |
| 28 | run_command | {
"command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"rich/highlighter.py\")\ns = p.read_text()\nfor i, line in enumerate(s.splitlines()):\n if \"url\" in line and \"https\" in line:\n print(f\"Line {i+1}: {line}\")\nPY\n"
} | {"status": "ok", "stdout": "Line 101: r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\",\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Line 101: r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\",\n", "stderr": "", "exit_code": 0} |
| 29 | run_command | {
"command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"rich/highlighter.py\")\ns = p.read_text()\nold = ' r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\",'\nnew = ' r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)\",'\nif old in s:\n s = s.replace(old, new)\n p.write_text(s)\n print(\"Replaced\")\nelse:\n print(\"Not found\")\nPY\n"
} | {"status": "ok", "stdout": "Replaced\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Replaced\n", "stderr": "", "exit_code": 0} |
| 30 | run_command | {
"command": "python3 -m py_compile rich/highlighter.py"
} | {"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0} |
| 31 | run_command | {
"command": "python3 - <<'PY'\nfrom rich.highlighter import ReprHighlighter\nfrom rich.text import Text\n\ntext = Text(\"https://user:pass@example.com\")\nhighlighter = ReprHighlighter()\nhighlighter.highlight(text)\nprint(f\"Spans: {text.spans}\")\nfor span in text.spans:\n print(f\"Span: {span}, Text: {text.plain[span.start:span.end]}\")\nPY\n"
} | {"status": "ok", "stdout": "Spans: [Span(0, 29, 'repr.url')]\nSpan: Span(0, 29, 'repr.url'), Text: https://user:pass@example.com\n", "stderr": "", "exit_code": full{"status": "ok", "stdout": "Spans: [Span(0, 29, 'repr.url')]\nSpan: Span(0, 29, 'repr.url'), Text: https://user:pass@example.com\n", "stderr": "", "exit_code": 0} |
| 32 | run_command | {
"command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"tests/test_highlighter.py\")\ns = p.read_text()\n# Find where highlight_tests is defined.\n# It's a list of tuples (test, spans).\n# I want to add a case for URL with @.\n# Looking at the file, highlight_tests is likely defined before test_highlight_regex.\n# Let's find the end of the list.\nimport re\nmatch = re.search(r'highlight_tests = \\[', s)\nif match:\n start = match.start()\n # Find the closing bracket of the list.\n # This is tricky because there are nested brackets.\n # Let's just find the last '],' before @pytest.mark.parametrize\n end_match = re.search(r'\\]\\s*\\n\\n\\n@pytest.mark.parametrize', s)\n if end_match:\n end = end_match.start()\n list_content = s[start:end+1]\n print(f\"List content length: {len(list_content)}\")\nPY\n"
} | {"status": "ok", "stdout": "List content length: 4610\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "List content length: 4610\n", "stderr": "", "exit_code": 0} |
| 33 | run_command | {
"command": "sed -n '150,160p' tests/test_highlighter.py"
} | {"status": "ok", "stdout": " ('\"\"', [Span(0, 2, \"repr.str\")]),\n ('\"hello\"', [Span(0, 7, \"repr.str\")]),\n ('\"\"\"hello\"\"\"', [Span(0, 11, \"repr.str\full{"status": "ok", "stdout": " ('\"\"', [Span(0, 2, \"repr.str\")]),\n ('\"hello\"', [Span(0, 7, \"repr.str\")]),\n ('\"\"\"hello\"\"\"', [Span(0, 11, \"repr.str\")]),\n (\"\\\\'foo'\", []),\n (\"it's no 'string'\", [Span(8, 16, \"repr.str\")]),\n (\"78351748-9b32-4e08-ad3e-7e9ff124d541\", [Span(0, 36, \"repr.uuid\")]),\n]\n\n\n@pytest.mark.parametrize(\"test, spans\", highlight_tests)\ndef test_highlight_regex(test: str, spans: List[Span]):\n", "stderr": "", "exit_code": 0} |
| 34 | run_command | {
"command": "python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"tests/test_highlighter.py\")\ns = p.read_text()\nold = ' (\"78351748-9b32-4e08-ad3e-7e9ff124d541\", [Span(0, 36, \"repr.uuid\")]),'\nnew = ' (\"78351748-9b32-4e08-ad3e-7e9ff124d541\", [Span(0, 36, \"repr.uuid\")]),\\n (\"https://user:pass@example.com\", [Span(0, 29, \"repr.url\")]),'\nif old in s:\n s = s.replace(old, new)\n p.write_text(s)\n print(\"Added test case\")\nelse:\n print(\"Not found\")\nPY\n"
} | {"status": "ok", "stdout": "Added test case\n", "stderr": "", "exit_code": 0}full{"status": "ok", "stdout": "Added test case\n", "stderr": "", "exit_code": 0} |
| 35 | run_command | {
"command": "python3 -m pytest tests/test_highlighter.py -k test_highlight_regex"
} | {"status": "ok", "stdout": "============================= test session starts ==============================\nplatform darwin -- Python 3.13.15, pytest-8.3.4, pfull{"status": "ok", "stdout": "============================= test session starts ==============================\nplatform darwin -- Python 3.13.15, pytest-8.3.4, pluggy-1.6.0\nrootdir: /private/workspace/tests\nconfigfile: pytest.ini\ncollected 83 items / 32 deselected / 51 selected\n\ntests/test_highlighter.py .............................................. [ 90%]\n..... [100%]\n\n====================== 51 passed, 32 deselected in 0.07s =======================\n", "stderr": "", "exit_code": 0} |
| 36 | run_command | {
"command": "git diff"
} | {"status": "ok", "stdout": "diff --git a/rich/highlighter.py b/rich/highlighter.py\nindex 27714b25..e4c462e2 100644\n--- a/rich/highlighter.py\n+++ b/rich/highlfull{"status": "ok", "stdout": "diff --git a/rich/highlighter.py b/rich/highlighter.py\nindex 27714b25..e4c462e2 100644\n--- a/rich/highlighter.py\n+++ b/rich/highlighter.py\n@@ -98,7 +98,7 @@ class ReprHighlighter(RegexHighlighter):\n r\"(?P<number>(?<!\\w)\\-?[0-9]+\\.?[0-9]*(e[-+]?\\d+?)?\\b|0x[0-9a-fA-F]*)\",\n r\"(?P<path>\\B(/[-\\w._+]+)*\\/)(?P<filename>[-\\w._+]*)?\",\n r\"(?<![\\\\\\w])(?P<str>b?'''.*?(?<!\\\\)'''|b?'.*?(?<!\\\\)'|b?\\\"\\\"\\\".*?(?<!\\\\)\\\"\\\"\\\"|b?\\\".*?(?<!\\\\)\\\")\",\n- r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)\",\n+ r\"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)\",\n ),\n ]\n \ndiff --git a/tests/test_highlighter.py b/tests/test_highlighter.py\nindex 30851cf7..b14ed468 100644\n--- a/tests/test_highlighter.py\n+++ b/tests/test_highlighter.py\n@@ -153,6 +153,7 @@ highlight_tests = [\n (\"\\\\'foo'\", []),\n (\"it's no 'string'\", [Span(8, 16, \"repr.str\")]),\n (\"78351748-9b32-4e08-ad3e-7e9ff124d541\", [Span(0, 36, \"repr.uuid\")]),\n+ (\"https://user:pass@example.com\", [Span(0, 29, \"repr.url\")]),\n ]\n \n \n", "stderr": "", "exit_code": 0} |
| 37 | run_command | {
"command": "git status"
} | {"status": "ok", "stdout": "On branch main\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <full{"status": "ok", "stdout": "On branch main\nChanges not staged for commit:\n (use \"git add <file>...\" to update what will be committed)\n (use \"git restore <file>...\" to discard changes in working directory)\n\tmodified: rich/highlighter.py\n\tmodified: tests/test_highlighter.py\n\nno changes added to commit (use \"git add\" and/or \"git commit -a\")\n", "stderr": "", "exit_code": 0} |
| 38 | submit_patch | {} | {"status": "ok", "patch_size": 1085, "files_changed": 2}full{"status": "ok", "patch_size": 1085, "files_changed": 2} |
diff --git a/rich/highlighter.py b/rich/highlighter.py
index 27714b25..e4c462e2 100644
--- a/rich/highlighter.py
+++ b/rich/highlighter.py
@@ -98,7 +98,7 @@ class ReprHighlighter(RegexHighlighter):
r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[-+]?\d+?)?\b|0x[0-9a-fA-F]*)",
r"(?P<path>\B(/[-\w._+]+)*\/)(?P<filename>[-\w._+]*)?",
r"(?<![\\\w])(?P<str>b?'''.*?(?<!\\)'''|b?'.*?(?<!\\)'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")",
- r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~]*)",
+ r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)",
),
]
diff --git a/tests/test_highlighter.py b/tests/test_highlighter.py
index 30851cf7..b14ed468 100644
--- a/tests/test_highlighter.py
+++ b/tests/test_highlighter.py
@@ -153,6 +153,7 @@ highlight_tests = [
("\\'foo'", []),
("it's no 'string'", [Span(8, 16, "repr.str")]),
("78351748-9b32-4e08-ad3e-7e9ff124d541", [Span(0, 36, "repr.uuid")]),
+ ("https://user:pass@example.com", [Span(0, 29, "repr.url")]),
]
........................................................................ [ 86%] ........... [100%] 83 passed in 0.11s