Repository: PyCQA/eradicate

Issue:
False positives in inline script metadata
Some code in [inline script metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) is mistakenly recognized as commented-out code, for example the `dependencies` field.

Running `eradicate` on [the example](https://packaging.python.org/en/latest/specifications/inline-script-metadata/#example) leads to the following result:

```sh
$ eradicate example.py
--- before/example.py
+++ after/example.py
@@ -1,9 +1,7 @@
 # /// script
 # requires-python = ">=3.11"
-# dependencies = [
 #   "requests<3",
 #   "rich",
-# ]
 # ///

 import requests
```

Discussion:

Solution patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 398abf5..e95fc57 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,15 +16,15 @@ jobs:
       fail-fast: false
       matrix:
         python-version:
-          - "3.7"
-          - "3.8"
           - "3.9"
           - "3.10"
           - "3.11"
+          - "3.12"
+          - "3.13"
 
           - "pypy-2.7"
-          - "pypy-3.7"
-          - "pypy-3.8"
+          - "pypy-3.10"
+          - "pypy-3.11"
     steps:
       - uses: actions/checkout@v3
 
@@ -91,32 +91,3 @@ jobs:
           # Integration:
           python -m eradicate setup.py
           eradicate setup.py
-
-
-  python2:
-    name: Tests with python ${{ matrix.python-version }}
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        python-version:
-          - "2.7"
-    steps:
-      - uses: actions/checkout@v3
-
-      - name: Set up python version ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
-        with:
-          python-version: ${{ matrix.python-version }}
-
-      - name: Install project and deps
-        run: |
-          pip install mock
-          python setup.py install
-
-      - name: Run tests
-        run: |
-          python test_eradicate.py
-          # Integration:
-          python -m eradicate setup.py
-          eradicate setup.py
diff --git a/eradicate.py b/eradicate.py
index 94f4f7f..c2a27c0 100644
--- a/eradicate.py
+++ b/eradicate.py
@@ -51,6 +51,7 @@ class Eradicator(object):
     PARTIAL_DICTIONARY_REGEX = re.compile(r'^\s*[\'"]\w+[\'"]\s*:.+[,{]\s*$')
     PRINT_RETURN_REGEX = re.compile(r'^(print|return)\b\s*')
     WITH_STATEMENT_REGEX = re.compile(r"with .+ as [a-zA-Z_][a-zA-Z0-9_]*:$")
+    INLINE_SCRIPT_METADATA = re.compile(r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$')
 
     CODE_INDICATORS = ['(', ')', '[', ']', '{', '}', ':', '=', '%',
                        'print', 'return', 'break', 'continue', 'import']
@@ -155,8 +156,20 @@ class Eradicator(object):
         return False
 
 
+    def inline_script_metadata_ranges(self, source):
+        """Return a list of ranges of lines of inline script metadata."""
+        return [
+            range(
+                source.count('\n', 0, match.start()),
+                source.count('\n', 0, match.end()) + 1,
+            )
+            for match in self.INLINE_SCRIPT_METADATA.finditer(source)
+        ]
+
+
     def commented_out_code_line_numbers(self, source, aggressive=True):
         """Yield line numbers of commented-out code."""
+        inline_script_metadata_ranges = self.inline_script_metadata_ranges(source)
         sio = io.StringIO(source)
         try:
             for token in tokenize.generate_tokens(sio.readline):
@@ -166,6 +179,7 @@ class Eradicator(object):
 
                 if (token_type == tokenize.COMMENT and
                         line.lstrip().startswith('#') and
+                        not any(start_row in r for r in inline_script_metadata_ranges) and
                         self.comment_contains_code(line, aggressive)):
                     yield start_row
         except (tokenize.TokenError, IndentationError):


Test patch:
diff --git a/test_eradicate.py b/test_eradicate.py
index a676001..4167913 100755
--- a/test_eradicate.py
+++ b/test_eradicate.py
@@ -445,6 +445,42 @@ y = 1  # x = 3
         eradicator.update_whitelist(["foo"], False)
         self.assertTrue(eradicator.WHITELIST_REGEX == re.compile("foo", flags=re.IGNORECASE))
 
+    def test_inline_script_metadata(self):
+        self.assertEqual(
+            """\
+# /// script
+# requires-python = ">=3.11"
+# dependencies = [
+#   "requests<3",
+#   "rich",
+# ]
+# ///
+
+import requests
+from rich.pretty import pprint
+
+resp = requests.get("https://peps.python.org/api/peps.json")
+data = resp.json()
+pprint([(k, v["title"]) for k, v in data.items()][:10])
+""",
+            ''.join(eradicate.Eradicator().filter_commented_out_code(
+                """\
+# /// script
+# requires-python = ">=3.11"
+# dependencies = [
+#   "requests<3",
+#   "rich",
+# ]
+# ///
+
+import requests
+from rich.pretty import pprint
+
+resp = requests.get("https://peps.python.org/api/peps.json")
+data = resp.json()
+pprint([(k, v["title"]) for k, v in data.items()][:10])
+""")))
+
 
 class SystemTests(unittest.TestCase):