Repository: scrapy/parsel

Issue:
.remove() also removes text after the deleted element
I tried removing an element as a way to exclude some repeated text from a website. I used the following code:

```python
import parsel

html = """
<html><body>
Text before.
<span>Text in.</span>
Text after.
</body></html>
"""

s = parsel.Selector(html)
s.css('span').remove()

print(s.get())

```
results in:
```html
<html><body>
Text before.
</body></html>
```

I would expect only the span to be removed, and the text after it to be left as-is, but it always removes the "text after" either until another element is encountered or it hits the end of the parent of the removed one.

Discussion:
Laerte: @Gallaecio Can you take a look? 😄
Laerte: @kmike Can you take a look?
codecov[bot]: # [Codecov](https://codecov.io/gh/scrapy/parsel/pull/247?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) Report
> Merging [#247](https://codecov.io/gh/scrapy/parsel/pull/247?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) (9a28261) into [master](https://codecov.io/gh/scrapy/parsel/commit/52b1db6f1c0e9768334d9c33627773e0e687a60d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) (52b1db6) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff            @@
##            master      #247   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            5         5           
  Lines          290       290           
  Branches        59        59           
=========================================
  Hits           290       290           
```


| [Impacted Files](https://codecov.io/gh/scrapy/parsel/pull/247?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy) | Coverage Δ | |
|---|---|---|
| [parsel/selector.py](https://codecov.io/gh/scrapy/parsel/pull/247/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy#diff-cGFyc2VsL3NlbGVjdG9yLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/scrapy/parsel/pull/247?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/scrapy/parsel/pull/247?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy). Last update [52b1db6...9a28261](https://codecov.io/gh/scrapy/parsel/pull/247?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scrapy).

Laerte: @kmike @Gallaecio @wRAR  Sorry for bother, can you guys check this PR? Thank you! 🤝 
kmike: Thanks @Laerte!

Patch:
diff --git a/docs/usage.rst b/docs/usage.rst
index d0a6fb0..dcef13d 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -400,7 +400,7 @@ Removing elements
 -----------------
 
 If for any reason you need to remove elements based on a Selector or
-a SelectorList, you can do it with the ``remove()`` method, available for both
+a SelectorList, you can do it with the ``drop()`` method, available for both
 classes.
 
 .. warning:: this is a destructive action and cannot be undone. The original
@@ -425,7 +425,7 @@ Example removing an ad from a blog post:
     >>> sel = Selector(text=doc)
     >>> sel.xpath('//div/text()').getall()
     ['Content paragraph...', '\n        ', '\n            Ad content...\n            ', '\n        ', '\n    ', 'More content...']
-    >>> sel.xpath('//div[@class="ad"]').remove()
+    >>> sel.xpath('//div[@class="ad"]').drop()
     >>> sel.xpath('//div//text()').getall()
     ['Content paragraph...', 'More content...']
 
diff --git a/parsel/selector.py b/parsel/selector.py
index e0d5a40..b84b030 100644
--- a/parsel/selector.py
+++ b/parsel/selector.py
@@ -4,13 +4,14 @@ XPath selectors based on lxml
 
 import typing
 import warnings
-from typing import Any, Dict, List, Optional, Mapping, Pattern, Union
+from typing import Any, Dict, List, Mapping, Optional, Pattern, Union
+from warnings import warn
 
 from lxml import etree, html
 from pkg_resources import parse_version
 
-from .utils import flatten, iflatten, extract_regex, shorten
-from .csstranslator import HTMLTranslator, GenericTranslator
+from .csstranslator import GenericTranslator, HTMLTranslator
+from .utils import extract_regex, flatten, iflatten, shorten
 
 _SelectorType = typing.TypeVar("_SelectorType", bound="Selector")
 
@@ -27,6 +28,10 @@ class CannotRemoveElementWithoutParent(Exception):
     pass
 
 
+class CannotDropElementWithoutParent(CannotRemoveElementWithoutParent):
+    pass
+
+
 class SafeXMLParser(etree.XMLParser):
     def __init__(self, *args, **kwargs) -> None:
         kwargs.setdefault("resolve_entities", False)
@@ -236,9 +241,21 @@ class SelectorList(List[_SelectorType]):
         """
         Remove matched nodes from the parent for each element in this list.
         """
+        warn(
+            "Method parsel.selector.SelectorList.remove is deprecated, please use parsel.selector.SelectorList.drop method instead",
+            category=DeprecationWarning,
+            stacklevel=2,
+        )
         for x in self:
             x.remove()
 
+    def drop(self) -> None:
+        """
+        Drop matched nodes from the parent for each element in this list.
+        """
+        for x in self:
+            x.drop()
+
 
 class Selector:
     """
@@ -503,6 +520,11 @@ class Selector:
         """
         Remove matched nodes from the parent element.
         """
+        warn(
+            "Method parsel.selector.Selector.remove is deprecated, please use parsel.selector.Selector.drop method instead",
+            category=DeprecationWarning,
+            stacklevel=2,
+        )
         try:
             parent = self.root.getparent()
         except AttributeError:
@@ -523,6 +545,30 @@ class Selector:
                 "are you trying to remove a root element?"
             )
 
+    def drop(self):
+        """
+        Drop matched nodes from the parent element.
+        """
+        try:
+            self.root.getparent()
+        except AttributeError:
+            # 'str' object has no attribute 'getparent'
+            raise CannotRemoveElementWithoutRoot(
+                "The node you're trying to drop has no root, "
+                "are you trying to drop a pseudo-element? "
+                "Try to use 'li' as a selector instead of 'li::text' or "
+                "'//li' instead of '//li/text()', for example."
+            )
+
+        try:
+            self.root.drop_tree()
+        except (AttributeError, AssertionError):
+            # 'NoneType' object has no attribute 'drop'
+            raise CannotDropElementWithoutParent(
+                "The node you're trying to remove has no parent, "
+                "are you trying to remove a root element?"
+            )
+
     @property
     def attrib(self) -> Dict[str, str]:
         """Return the attributes dictionary for underlying element."""


Test patch:
diff --git a/tests/test_selector.py b/tests/test_selector.py
index 99d9a55..d0bb281 100644
--- a/tests/test_selector.py
+++ b/tests/test_selector.py
@@ -1050,7 +1050,7 @@ class SelectorTestCase(unittest.TestCase):
             text="<html><body><ul><li>1</li><li>2</li><li>3</li></ul></body></html>"
         )
         sel_list = sel.css("li")
-        sel_list.remove()
+        sel_list.drop()
         self.assertIsSelectorList(sel.css("li"))
         self.assertEqual(sel.css("li"), [])
 
@@ -1059,7 +1059,7 @@ class SelectorTestCase(unittest.TestCase):
             text="<html><body><ul><li>1</li><li>2</li><li>3</li></ul></body></html>"
         )
         sel_list = sel.css("li")
-        sel_list[0].remove()
+        sel_list[0].drop()
         self.assertIsSelectorList(sel.css("li"))
         self.assertEqual(sel.css("li::text").getall(), ["2", "3"])
 
@@ -1070,7 +1070,7 @@ class SelectorTestCase(unittest.TestCase):
         sel_list = sel.css("li::text")
         self.assertEqual(sel_list.getall(), ["1", "2", "3"])
         with self.assertRaises(CannotRemoveElementWithoutRoot):
-            sel_list.remove()
+            sel_list.drop()
 
         self.assertIsSelectorList(sel.css("li"))
         self.assertEqual(sel.css("li::text").getall(), ["1", "2", "3"])
@@ -1082,7 +1082,7 @@ class SelectorTestCase(unittest.TestCase):
         sel_list = sel.css("li::text")
         self.assertEqual(sel_list.getall(), ["1", "2", "3"])
         with self.assertRaises(CannotRemoveElementWithoutRoot):
-            sel_list[0].remove()
+            sel_list[0].drop()
 
         self.assertIsSelectorList(sel.css("li"))
         self.assertEqual(sel.css("li::text").getall(), ["1", "2", "3"])
@@ -1094,15 +1094,15 @@ class SelectorTestCase(unittest.TestCase):
         sel_list = sel.css("li::text")
         self.assertEqual(sel_list.getall(), ["1", "2", "3"])
         with self.assertRaises(CannotRemoveElementWithoutParent):
-            sel.remove()
+            sel.drop()
 
         with self.assertRaises(CannotRemoveElementWithoutParent):
-            sel.css("html").remove()
+            sel.css("html").drop()
 
         self.assertIsSelectorList(sel.css("li"))
         self.assertEqual(sel.css("li::text").getall(), ["1", "2", "3"])
 
-        sel.css("body").remove()
+        sel.css("body").drop()
         self.assertEqual(sel.get(), "<html></html>")
 
     def test_deep_nesting(self):
@@ -1316,3 +1316,13 @@ class ExsltTestCase(unittest.TestCase):
             ).extract(),
             ["url", "name", "startDate", "location", "offers"],
         )
+
+    def test_dont_remove_text_after_deleted_element(self) -> None:
+        sel = self.sscls(
+            text="""<html><body>Text before.<span>Text in.</span> Text after.</body></html>
+            """
+        )
+        sel.css("span").drop()
+        self.assertEqual(
+            sel.get(), "<html><body>Text before. Text after.</body></html>"
+        )