Repository: PyCQA/flake8-bugbear

Issue:
B950: Propose exemption for long URLs
I like the idea of using B950 over E501, but I do not want to be warned anytime I include a long URL in a comment. Breaking URLs over multiple lines is not acceptable as I want them to be clickable.

pycodestyle addresses this by making an exemption to the line length error for comments that contain no whitespace. See [line 304](https://github.com/PyCQA/pycodestyle/blob/aa3417b6a51f5912e32d9c8c879e1b9dd660d5f8/pycodestyle.py#L304) (currently) of pycodestyle.py:

```
# Special case for long URLs in multi-line docstrings or
# comments, but still report the error when the 72 first chars
# are whitespaces.
chunks = line.split()
if ((len(chunks) == 1 and multiline) or
    (len(chunks) == 2 and chunks[0] == '#')) and \
        len(line) - len(chunks[-1]) < max_line_length - 7:
    return
```

I propose adding a similar exemption to flake8-bugbear. Of course, I could add a `# noqa` comment after every URL comment, but I much prefer pycodestyle's solution.

Comments:

Patch:
diff --git a/.gitignore b/.gitignore
index fc8539e..1dc5815 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,7 @@ var/
 *.egg-info/
 .installed.cfg
 *.egg
+venv/
 
 # PyInstaller
 #  Usually these files are written by a python script from a template
diff --git a/README.rst b/README.rst
index 6d94ba7..7ad3874 100644
--- a/README.rst
+++ b/README.rst
@@ -173,7 +173,15 @@ significantly violate the line length, you will receive a message that
 states what the actual limit is. This is inspired by Raymond Hettinger's
 `"Beyond PEP 8" talk <https://www.youtube.com/watch?v=wf-BqAjZb8M>`_ and
 highway patrol not stopping you if you drive < 5mph too fast. Disable
-E501 to avoid duplicate warnings.
+E501 to avoid duplicate warnings. Like E501, this error ignores long shebangs
+on the first line and urls or paths that are on their own line::
+
+  #! long shebang ignored
+
+  # https://some-super-long-domain-name.com/with/some/very/long/paths
+  url = (
+      "https://some-super-long-domain-name.com/with/some/very/long/paths"
+  )
 
 
 How to enable opinionated warnings
@@ -237,6 +245,11 @@ MIT
 Change Log
 ----------
 
+21.12.0
+~~~~~~~~~~
+
+* B950: Add same special cases as E501 (#213)
+
 21.11.29
 ~~~~~~~~~~
 
diff --git a/bugbear.py b/bugbear.py
index eab2b53..78c2ca3 100644
--- a/bugbear.py
+++ b/bugbear.py
@@ -67,8 +67,31 @@ class BugBearChecker:
         The following simple checks are based on the raw lines, not the AST.
         """
         for lineno, line in enumerate(self.lines, start=1):
+            # Special case: ignore long shebang (following pycodestyle).
+            if lineno == 1 and line.startswith("#!"):
+                continue
+
             length = len(line) - 1
             if length > 1.1 * self.max_line_length:
+                # Special case long URLS and paths to follow pycodestyle.
+                # Would use the `pycodestyle.maximum_line_length` directly but
+                # need to supply it arguments that are not available so chose
+                # to replicate instead.
+                chunks = line.split()
+
+                is_line_comment_url_path = len(chunks) == 2 and chunks[0] == "#"
+
+                just_long_url_path = len(chunks) == 1
+
+                num_leading_whitespaces = len(line) - len(chunks[-1])
+                too_many_leading_white_spaces = (
+                    num_leading_whitespaces >= self.max_line_length - 7
+                )
+
+                skip = is_line_comment_url_path or just_long_url_path
+                if skip and not too_many_leading_white_spaces:
+                    continue
+
                 yield B950(lineno, length, vars=(length, self.max_line_length))
 
     @classmethod


Test patch:
diff --git a/tests/b950.py b/tests/b950.py
index 1939105..942e50c 100644
--- a/tests/b950.py
+++ b/tests/b950.py
@@ -1,3 +1,4 @@
+#! Ignore long shebang fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
 # Assumes the default allowed line length of 79
 
 "line is fine"
@@ -5,3 +6,16 @@
 "                              line               is        still fine          "
 "                              line               is    no longer fine by any measures, yup"
 "line is fine again"
+
+# Ensure URL/path on it's own line is fine
+"https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com"
+"NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com"
+# https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com
+# NOT OK: https://foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.com
+#
+#: Okay
+# This
+#                                                                     almost_empty_line_too_long
+
+# This
+#                                                                      almost_empty_line_too_long
diff --git a/tests/test_bugbear.py b/tests/test_bugbear.py
index 319a508..d1af992 100644
--- a/tests/test_bugbear.py
+++ b/tests/test_bugbear.py
@@ -318,7 +318,15 @@ class BugbearTestCase(unittest.TestCase):
         filename = Path(__file__).absolute().parent / "b950.py"
         bbc = BugBearChecker(filename=str(filename))
         errors = list(bbc.run())
-        self.assertEqual(errors, self.errors(B950(6, 92, vars=(92, 79))))
+        self.assertEqual(
+            errors,
+            self.errors(
+                B950(7, 92, vars=(92, 79)),
+                B950(12, 103, vars=(103, 79)),
+                B950(14, 103, vars=(103, 79)),
+                B950(21, 97, vars=(97, 79)),
+            ),
+        )
 
     def test_selfclean_bugbear(self):
         filename = Path(__file__).absolute().parent.parent / "bugbear.py"