psf / black

The uncompromising Python code formatter
https://black.readthedocs.io/en/stable/
MIT License
38.71k stars 2.43k forks source link

"INTERNAL ERROR: Black produced different code on the second pass of the formatter" #1629

Closed ambv closed 3 years ago

ambv commented 4 years ago

This is a rare problem that we're currently investigating. The most common case of it has to do with a combination of magic trailing commas and optional parentheses. Long story short, there's this behavior:

The expected behavior is that there should be no difference between the first formatting and the second formatting. In practice Black sometimes chooses for or against optional parentheses differently depending on whether the line should be exploded or not. This is what needs fixing.

Workaround

We're working on fixing this, until then, format the file twice with --fast, it will keep its formatting moving forward.

Call To Action

If you find a case of this, please attach the generated log here so we can investigate. We've already added three identifying examples of this as expected failures to https://github.com/psf/black/pull/1627/commits/25206d8cc6e98143f0b10bcbe9e8b41b8b543abe.

Finally, if you're interested in debugging this yourself, look for should_explode in if statements in the Black codebase. Those are the decisions that lead to unstable formatting.

mawkee commented 4 years ago

One case that I believe refers to this bug: (non-relevant parts removed)

Using the workaround (running with --fast) worked for my case.

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
-            if (
-                mission.get("status")
-                not in [
-                    "new",
-                    "test",
-                    "scheduled"
-                ]
-                and not mission.get("redeem")
-            ):
+            if mission.get("status") not in [
+                "new",
+                "test",
+                "scheduled",
+            ] and not mission.get("redeem"):
--- first pass
+++ second pass
@@ -7373,15 +7373,19 @@
-            if mission.get("status") not in [
-                "new",
-                "test",
-                "scheduled",
-            ] and not mission.get("redeem"):
+            if (
+                mission.get("status")
+                not in [
+                    "new",
+                    "test",
+                    "scheduled",
+                ]
+                and not mission.get("redeem")
+            ):
ichard26 commented 4 years ago

A few more cases that seem to be related (running with Black 20.08b1):

1

Source ```py3 if get_flag('WITH_PYMALLOC', lambda: impl == 'cp', warn=(impl == 'cp' and sys.version_info < (3, 8))) \ and sys.version_info < (3, 8): pass ```
Log ```diff Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False) --- source +++ first pass @@ -1,6 +1,6 @@ -if get_flag('WITH_PYMALLOC', - lambda: impl == 'cp', - warn=(impl == 'cp' and - sys.version_info < (3, 8))) \ - and sys.version_info < (3, 8): +if get_flag( + "WITH_PYMALLOC", + lambda: impl == "cp", + warn=(impl == "cp" and sys.version_info < (3, 8)), +) and sys.version_info < (3, 8): pass --- first pass +++ second pass @@ -1,6 +1,9 @@ -if get_flag( - "WITH_PYMALLOC", - lambda: impl == "cp", - warn=(impl == "cp" and sys.version_info < (3, 8)), -) and sys.version_info < (3, 8): +if ( + get_flag( + "WITH_PYMALLOC", + lambda: impl == "cp", + warn=(impl == "cp" and sys.version_info < (3, 8)), + ) + and sys.version_info < (3, 8) +): pass ```

2

Source ```py3 if get_flag('Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff, expected=4, warn=(impl == 'cp' and sys.version_info < (3, 3))) \ and sys.version_info < (3, 3): pass ```
Log ```diff Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False) --- source +++ first pass @@ -1,7 +1,7 @@ -if get_flag('Py_UNICODE_SIZE', - lambda: sys.maxunicode == 0x10ffff, - expected=4, - warn=(impl == 'cp' and - sys.version_info < (3, 3))) \ - and sys.version_info < (3, 3): +if get_flag( + "Py_UNICODE_SIZE", + lambda: sys.maxunicode == 0x10FFFF, + expected=4, + warn=(impl == "cp" and sys.version_info < (3, 3)), +) and sys.version_info < (3, 3): pass --- first pass +++ second pass @@ -1,7 +1,10 @@ -if get_flag( - "Py_UNICODE_SIZE", - lambda: sys.maxunicode == 0x10FFFF, - expected=4, - warn=(impl == "cp" and sys.version_info < (3, 3)), -) and sys.version_info < (3, 3): +if ( + get_flag( + "Py_UNICODE_SIZE", + lambda: sys.maxunicode == 0x10FFFF, + expected=4, + warn=(impl == "cp" and sys.version_info < (3, 3)), + ) + and sys.version_info < (3, 3) +): pass ```

3

Source ```py3 should_list_installed = ( subcommand_name in ['show', 'uninstall'] and not current.startswith('-') ) ```
Log ```diff Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False) --- source +++ first pass @@ -1,4 +1,4 @@ -should_list_installed = ( - subcommand_name in ['show', 'uninstall'] and - not current.startswith('-') -) +should_list_installed = subcommand_name in [ + "show", + "uninstall", +] and not current.startswith("-") --- first pass +++ second pass @@ -1,4 +1,8 @@ -should_list_installed = subcommand_name in [ - "show", - "uninstall", -] and not current.startswith("-") +should_list_installed = ( + subcommand_name + in [ + "show", + "uninstall", + ] + and not current.startswith("-") +) ```
ryanhiebert commented 4 years ago

Possibly related, though in my codebase it all seems to be right around uses of a trailing # noqa to make flake8 happy with long lines from before I was using black. The workaround did the trick to get me unstuck.

The log output ```diff Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False) --- source +++ first pass @@ -1397,12 +1397,14 @@ has_submissions_map = self.get_submissions_map(submissions_q) # Find all courses and their instructors. This is because the ungraded # submissions won't have grader specified. Therefore the submissions # are counted against each of the course's instructors. - course_users = self.retrieval.enrollment_scores.instructors().values_list( # noqa - "enrollment__endpoint_course_id", "enrollment__endpoint_user_id" + course_users = ( + self.retrieval.enrollment_scores.instructors().values_list( # noqa + "enrollment__endpoint_course_id", "enrollment__endpoint_user_id" + ) ) result = {} # Assign the number of ungraded submissions to each of the course's # instructors. @@ -1493,12 +1495,12 @@ # Add the insight_course to the select related. course_date_prefetch = EndpointCourse.prefetch_dates( "endpoint_course", self.retrieval.endpoint ) - course_date_prefetch.queryset = course_date_prefetch.queryset.select_related( # noqa - "insight_course" + course_date_prefetch.queryset = ( + course_date_prefetch.queryset.select_related("insight_course") # noqa ) if lms[self.retrieval.endpoint.lms_config.lms].limits_course_access_data(): course_date_prefetch.queryset = course_date_prefetch.queryset.annotate( course_access_limit_date=Subquery( self.retrieval.endpoint.retrievals.filter( --- first pass +++ second pass @@ -1495,13 +1495,13 @@ # Add the insight_course to the select related. course_date_prefetch = EndpointCourse.prefetch_dates( "endpoint_course", self.retrieval.endpoint ) - course_date_prefetch.queryset = ( - course_date_prefetch.queryset.select_related("insight_course") # noqa - ) + course_date_prefetch.queryset = course_date_prefetch.queryset.select_related( + "insight_course" + ) # noqa if lms[self.retrieval.endpoint.lms_config.lms].limits_course_access_data(): course_date_prefetch.queryset = course_date_prefetch.queryset.annotate( course_access_limit_date=Subquery( self.retrieval.endpoint.retrievals.filter( courses__endpoint_course_id=OuterRef("id"), ```
serhiy-storchaka commented 4 years ago

I encountered this bug when trying to remove trailing commas. When in fragment (formatted with Black)

assert (
    xxxxxx(
        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
        xxxxxxxxxxxxxxxxxxxxxxxxx,
    )
    == xxxxxx(xxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx)
)

remove the trailing comma at line 4, it breaks Black:

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,7 +1,4 @@
-assert (
-    xxxxxx(
-        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
-        xxxxxxxxxxxxxxxxxxxxxxxxx
-    )
-    == xxxxxx(xxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx)
-)
+assert xxxxxx(
+    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+    xxxxxxxxxxxxxxxxxxxxxxxxx,
+) == xxxxxx(xxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx)
--- first pass
+++ second pass
@@ -1,4 +1,7 @@
-assert xxxxxx(
-    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
-    xxxxxxxxxxxxxxxxxxxxxxxxx,
-) == xxxxxx(xxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx)
+assert (
+    xxxxxx(
+        xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
+        xxxxxxxxxxxxxxxxxxxxxxxxx,
+    )
+    == xxxxxx(xxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx)
+)

The workaround produces the result with the trailing comma which differs from the original:

assert xxxxxx(
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,
    xxxxxxxxxxxxxxxxxxxxxxxxx,
) == xxxxxx(xxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx)
sethmlarson commented 4 years ago

Source:

    @staticmethod
    def _pd_dtype_to_es_dtype(pd_dtype):
        """
        Mapping pandas dtypes to Elasticsearch dtype
        --------------------------------------------
    Pandas dtype    Python type NumPy type  Usage
    object  str string_, unicode_   Text
    int64   int int_, int8, int16, int32, int64, uint8, uint16, uint32, uint64  Integer numbers
    float64 float   float_, float16, float32, float64   Floating point numbers
    bool    bool    bool_   True/False values
    datetime64  NA  datetime64[ns]  Date and time values
    timedelta[ns]   NA  NA  Differences between two datetimes
    category    NA  NA  Finite list of text values
    ```
    """

(lots of tabs in the docstring)

Log:

```diff
--- src
+++ dst
@@ -5280,11 +5280,11 @@
           body=
             Expr(
               value=
                 Constant(
                   value=
-                    'Mapping pandas dtypes to Elasticsearch dtype\n--------------------------------------------\n\n```\nPandas dtype\tPython type\tNumPy type\tUsage\nobject\tstr\tstring_, unicode_\tText\nint64\tint\tint_, int8, int16, int32, int64, uint8, uint16, uint32, uint64\tInteger numbers\nfloat64\tfloat\tfloat_, float16, float32, float64\tFloating point numbers\nbool\tbool\tbool_\tTrue/False values\ndatetime64\tNA\tdatetime64[ns]\tDate and time values\ntimedelta[ns]\tNA\tNA\tDifferences between two datetimes\ncategory\tNA\tNA\tFinite list of text values\n```',  # str
+                    'Mapping pandas dtypes to Elasticsearch dtype\n--------------------------------------------\n\n```\nPandas dtype    Python type     NumPy type      Usage\nobject  str     string_, unicode_       Text\nint64   int     int_, int8, int16, int32, int64, uint8, uint16, uint32, uint64  Integer numbers\nfloat64 float   float_, float16, float32, float64       Floating point numbers\nbool    bool    bool_   True/False values\ndatetime64      NA      datetime64[ns]  Date and time values\ntimedelta[ns]   NA      NA      Differences between two datetimes\ncategory        NA      NA      Finite list of text values\n```',  # str
                 )  # /Constant
             )  # /Expr
             Assign(
               targets=
                 Name(
ichard26 commented 4 years ago

@sethmlarson that is case of #1601. It isn't unstable formatting where there are differences between the first and second pass of Black, just that Black fails its own AST safety checks.

matthewfeickert commented 4 years ago

We (cc @kratsg @lukasheinrich) have one from pyhf found in https://github.com/scikit-hep/pyhf/pull/1048 and produced by the following diff in the codebase

$ git diff
diff --git a/tests/test_tensor.py b/tests/test_tensor.py
index 8cbfdead..e6b3afc6 100644
--- a/tests/test_tensor.py
+++ b/tests/test_tensor.py
@@ -82,7 +82,7 @@ def test_complex_tensor_ops(backend):
             tb.where(
                 tb.astensor([1, 0, 1], dtype="bool"),
                 tb.astensor([1, 1, 1]),
-                tb.astensor([2, 2, 2]),
+                tb.astensor([2, 2, 2])
             )
         )
         == [1, 2, 1]

Black error diff:

Mode(target_versions={<TargetVersion.PY36: 6>, <TargetVersion.PY38: 8>, <TargetVersion.PY37: 7>}, line_length=88, string_normalization=False, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -75,20 +75,17 @@
         -1,
         0,
         1,
         1,
     ]
-    assert (
-        tb.tolist(
-            tb.where(
-                tb.astensor([1, 0, 1], dtype="bool"),
-                tb.astensor([1, 1, 1]),
-                tb.astensor([2, 2, 2])
-            )
-        )
-        == [1, 2, 1]
-    )
+    assert tb.tolist(
+        tb.where(
+            tb.astensor([1, 0, 1], dtype="bool"),
+            tb.astensor([1, 1, 1]),
+            tb.astensor([2, 2, 2]),
+        )
+    ) == [1, 2, 1]

 def test_ones(backend):
     tb = pyhf.tensorlib
     assert tb.tolist(tb.ones((2, 3))) == [[1, 1, 1], [1, 1, 1]]
--- first pass
+++ second pass
@@ -75,17 +75,20 @@
         -1,
         0,
         1,
         1,
     ]
-    assert tb.tolist(
-        tb.where(
-            tb.astensor([1, 0, 1], dtype="bool"),
-            tb.astensor([1, 1, 1]),
-            tb.astensor([2, 2, 2]),
-        )
-    ) == [1, 2, 1]
+    assert (
+        tb.tolist(
+            tb.where(
+                tb.astensor([1, 0, 1], dtype="bool"),
+                tb.astensor([1, 1, 1]),
+                tb.astensor([2, 2, 2]),
+            )
+        )
+        == [1, 2, 1]
+    )

 def test_ones(backend):
     tb = pyhf.tensorlib
     assert tb.tolist(tb.ones((2, 3))) == [[1, 1, 1], [1, 1, 1]]
sarthakvk commented 4 years ago

diff in codebase

diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py
index 316a299ba..c70eacdfa 100644
--- a/pandas/tests/scalar/timestamp/test_constructors.py
+++ b/pandas/tests/scalar/timestamp/test_constructors.py
@@ -267,7 +267,7 @@ class TestTimestampConstructors:
                 hour=1,
                 minute=2,
                 second=3,
-                microsecond=999999,
+                microsecond=999999
             )
         ) == repr(Timestamp("2015-11-12 01:02:03.999999"))

~

generated log

Mode(target_versions={<TargetVersion.PY37: 7>, <TargetVersion.PY38: 8>}, line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -257,22 +257,21 @@

         assert repr(Timestamp(year=2015, month=11, day=12)) == repr(
             Timestamp("20151112")
         )

-        assert(repr(
+        assert repr(
             Timestamp(
                 year=2015,
                 month=11,
                 day=12,
                 hour=1,
                 minute=2,
                 second=3,
-                microsecond=999999
+                microsecond=999999,
             )
         ) == repr(Timestamp("2015-11-12 01:02:03.999999"))
-        )

     def test_constructor_fromordinal(self):
         base = datetime(2000, 1, 1)

         ts = Timestamp.fromordinal(base.toordinal(), freq="D")
--- first pass
+++ second pass
@@ -257,21 +257,24 @@

         assert repr(Timestamp(year=2015, month=11, day=12)) == repr(
             Timestamp("20151112")
         )

-        assert repr(
-            Timestamp(
-                year=2015,
-                month=11,
-                day=12,
-                hour=1,
-                minute=2,
-                second=3,
-                microsecond=999999,
+        assert (
+            repr(
+                Timestamp(
+                    year=2015,
+                    month=11,
+                    day=12,
+                    hour=1,
+                    minute=2,
+                    second=3,
+                    microsecond=999999,
+                )
             )
-        ) == repr(Timestamp("2015-11-12 01:02:03.999999"))
+            == repr(Timestamp("2015-11-12 01:02:03.999999"))
+        )

     def test_constructor_fromordinal(self):
         base = datetime(2000, 1, 1)

         ts = Timestamp.fromordinal(base.toordinal(), freq="D")
samkit-jain commented 4 years ago

Another case that I think is related to this issue:

Source:

if any(
    k in t
    for k in ["AAAAAAAAAA", "AAAAA", "AAAAAA", "AAAAAAAAA", "AAA", "AAAAAA", "AAAAAAAA", "AAA", "AAAAA", "AAAAA", "AAAA"]
) and not any(k in t for k in ["AAA"]):
    pass

Log:

Mode(target_versions={<TargetVersion.PY38: 8>}, line_length=120, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,5 +1,17 @@
 if any(
     k in t
-    for k in ["AAAAAAAAAA", "AAAAA", "AAAAAA", "AAAAAAAAA", "AAA", "AAAAAA", "AAAAAAAA", "AAA", "AAAAA", "AAAAA", "AAAA"]
+    for k in [
+        "AAAAAAAAAA",
+        "AAAAA",
+        "AAAAAA",
+        "AAAAAAAAA",
+        "AAA",
+        "AAAAAA",
+        "AAAAAAAA",
+        "AAA",
+        "AAAAA",
+        "AAAAA",
+        "AAAA",
+    ]
 ) and not any(k in t for k in ["AAA"]):
     pass
--- first pass
+++ second pass
@@ -1,17 +1,20 @@
-if any(
-    k in t
-    for k in [
-        "AAAAAAAAAA",
-        "AAAAA",
-        "AAAAAA",
-        "AAAAAAAAA",
-        "AAA",
-        "AAAAAA",
-        "AAAAAAAA",
-        "AAA",
-        "AAAAA",
-        "AAAAA",
-        "AAAA",
-    ]
-) and not any(k in t for k in ["AAA"]):
+if (
+    any(
+        k in t
+        for k in [
+            "AAAAAAAAAA",
+            "AAAAA",
+            "AAAAAA",
+            "AAAAAAAAA",
+            "AAA",
+            "AAAAAA",
+            "AAAAAAAA",
+            "AAA",
+            "AAAAA",
+            "AAAAA",
+            "AAAA",
+        ]
+    )
+    and not any(k in t for k in ["AAA"])
+):
     pass
twm commented 4 years ago

Original code:

https://github.com/twisted/twisted/blob/6546d06769e4ca93771608fd4cbdb81d0654aa95/src/twisted/words/test/test_irc.py#L1882-L1901

Instability:

--- first pass
+++ second pass
@@ -1748,20 +1748,23 @@
             False,
             12,
             timestamp,
             ["#fakeusers", "#fakemisc"],
         )
-        expected = "\r\n".join(
-            [
-                ":%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User",
-                ":%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server",
-                ":%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time",
-                ":%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc",
-                ":%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.",
-                "",
-            ]
-        ) % dict(hostname=hostname, timestamp=timestamp, req=req, targ=targ)
+        expected = (
+            "\r\n".join(
+                [
+                    ":%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User",
+                    ":%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server",
+                    ":%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time",
+                    ":%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc",
+                    ":%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.",
+                    "",
+                ]
+            )
+            % dict(hostname=hostname, timestamp=timestamp, req=req, targ=targ)
+        )
         self.check(expected)
Full log ```diff Mode(target_versions={, , , }, line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False) --- source +++ first pass @@ -36,652 +36,592 @@ if isinstance(val[0], str): bufferValue = [b.decode("utf8") for b in bufferValue] self.assertEqual(bufferValue, val) - class ModeParsingTests(IRCTestCase): """ Tests for L{twisted.words.protocols.irc.parseModes}. """ - paramModes = ('klb', 'b') - + + paramModes = ("klb", "b") def test_emptyModes(self): """ Parsing an empty mode string raises L{irc.IRCBadModes}. """ - self.assertRaises(irc.IRCBadModes, irc.parseModes, '', []) - + self.assertRaises(irc.IRCBadModes, irc.parseModes, "", []) def test_emptyModeSequence(self): """ Parsing a mode string that contains an empty sequence (either a C{+} or C{-} followed directly by another C{+} or C{-}, or not followed by anything at all) raises L{irc.IRCBadModes}. """ - self.assertRaises(irc.IRCBadModes, irc.parseModes, '++k', []) - self.assertRaises(irc.IRCBadModes, irc.parseModes, '-+k', []) - self.assertRaises(irc.IRCBadModes, irc.parseModes, '+', []) - self.assertRaises(irc.IRCBadModes, irc.parseModes, '-', []) - + self.assertRaises(irc.IRCBadModes, irc.parseModes, "++k", []) + self.assertRaises(irc.IRCBadModes, irc.parseModes, "-+k", []) + self.assertRaises(irc.IRCBadModes, irc.parseModes, "+", []) + self.assertRaises(irc.IRCBadModes, irc.parseModes, "-", []) def test_malformedModes(self): """ Parsing a mode string that does not start with C{+} or C{-} raises L{irc.IRCBadModes}. """ - self.assertRaises(irc.IRCBadModes, irc.parseModes, 'foo', []) - self.assertRaises(irc.IRCBadModes, irc.parseModes, '%', []) - + self.assertRaises(irc.IRCBadModes, irc.parseModes, "foo", []) + self.assertRaises(irc.IRCBadModes, irc.parseModes, "%", []) def test_nullModes(self): """ Parsing a mode string that contains no mode characters raises L{irc.IRCBadModes}. """ - self.assertRaises(irc.IRCBadModes, irc.parseModes, '+', []) - self.assertRaises(irc.IRCBadModes, irc.parseModes, '-', []) - + self.assertRaises(irc.IRCBadModes, irc.parseModes, "+", []) + self.assertRaises(irc.IRCBadModes, irc.parseModes, "-", []) def test_singleMode(self): """ Parsing a single mode setting with no parameters results in that mode, with no parameters, in the "added" direction and no modes in the "removed" direction. """ - added, removed = irc.parseModes('+s', []) - self.assertEqual(added, [('s', None)]) + added, removed = irc.parseModes("+s", []) + self.assertEqual(added, [("s", None)]) self.assertEqual(removed, []) - added, removed = irc.parseModes('-s', []) + added, removed = irc.parseModes("-s", []) self.assertEqual(added, []) - self.assertEqual(removed, [('s', None)]) - + self.assertEqual(removed, [("s", None)]) def test_singleDirection(self): """ Parsing a single-direction mode setting with multiple modes and no parameters, results in all modes falling into the same direction group. """ - added, removed = irc.parseModes('+stn', []) - self.assertEqual(added, [('s', None), - ('t', None), - ('n', None)]) + added, removed = irc.parseModes("+stn", []) + self.assertEqual(added, [("s", None), ("t", None), ("n", None)]) self.assertEqual(removed, []) - added, removed = irc.parseModes('-nt', []) + added, removed = irc.parseModes("-nt", []) self.assertEqual(added, []) - self.assertEqual(removed, [('n', None), - ('t', None)]) - + self.assertEqual(removed, [("n", None), ("t", None)]) def test_multiDirection(self): """ Parsing a multi-direction mode setting with no parameters. """ - added, removed = irc.parseModes('+s-n+ti', []) - self.assertEqual(added, [('s', None), - ('t', None), - ('i', None)]) - self.assertEqual(removed, [('n', None)]) - + added, removed = irc.parseModes("+s-n+ti", []) + self.assertEqual(added, [("s", None), ("t", None), ("i", None)]) + self.assertEqual(removed, [("n", None)]) def test_consecutiveDirection(self): """ Parsing a multi-direction mode setting containing two consecutive mode sequences with the same direction results in the same result as if there were only one mode sequence in the same direction. """ - added, removed = irc.parseModes('+sn+ti', []) - self.assertEqual(added, [('s', None), - ('n', None), - ('t', None), - ('i', None)]) + added, removed = irc.parseModes("+sn+ti", []) + self.assertEqual(added, [("s", None), ("n", None), ("t", None), ("i", None)]) self.assertEqual(removed, []) - def test_mismatchedParams(self): """ If the number of mode parameters does not match the number of modes expecting parameters, L{irc.IRCBadModes} is raised. """ - self.assertRaises(irc.IRCBadModes, - irc.parseModes, - '+k', [], - self.paramModes) - self.assertRaises(irc.IRCBadModes, - irc.parseModes, - '+kl', ['foo', '10', 'lulz_extra_param'], - self.paramModes) - + self.assertRaises(irc.IRCBadModes, irc.parseModes, "+k", [], self.paramModes) + self.assertRaises( + irc.IRCBadModes, + irc.parseModes, + "+kl", + ["foo", "10", "lulz_extra_param"], + self.paramModes, + ) def test_parameters(self): """ Modes which require parameters are parsed and paired with their relevant parameter, modes which do not require parameters do not consume any of the parameters. """ added, removed = irc.parseModes( - '+klbb', - ['somekey', '42', 'nick!user@host', 'other!*@*'], - self.paramModes) - self.assertEqual(added, [('k', 'somekey'), - ('l', '42'), - ('b', 'nick!user@host'), - ('b', 'other!*@*')]) + "+klbb", ["somekey", "42", "nick!user@host", "other!*@*"], self.paramModes + ) + self.assertEqual( + added, + [ + ("k", "somekey"), + ("l", "42"), + ("b", "nick!user@host"), + ("b", "other!*@*"), + ], + ) self.assertEqual(removed, []) added, removed = irc.parseModes( - '-klbb', - ['nick!user@host', 'other!*@*'], - self.paramModes) + "-klbb", ["nick!user@host", "other!*@*"], self.paramModes + ) self.assertEqual(added, []) - self.assertEqual(removed, [('k', None), - ('l', None), - ('b', 'nick!user@host'), - ('b', 'other!*@*')]) + self.assertEqual( + removed, + [("k", None), ("l", None), ("b", "nick!user@host"), ("b", "other!*@*")], + ) # Mix a no-argument mode in with argument modes. added, removed = irc.parseModes( - '+knbb', - ['somekey', 'nick!user@host', 'other!*@*'], - self.paramModes) - self.assertEqual(added, [('k', 'somekey'), - ('n', None), - ('b', 'nick!user@host'), - ('b', 'other!*@*')]) + "+knbb", ["somekey", "nick!user@host", "other!*@*"], self.paramModes + ) + self.assertEqual( + added, + [ + ("k", "somekey"), + ("n", None), + ("b", "nick!user@host"), + ("b", "other!*@*"), + ], + ) self.assertEqual(removed, []) - class MiscTests(IRCTestCase): """ Tests for miscellaneous functions. """ + def test_foldr(self): """ Apply a function of two arguments cumulatively to the items of a sequence, from right to left, so as to reduce the sequence to a single value. """ - self.assertEqual( - irc._foldr(operator.sub, 0, [1, 2, 3, 4]), - -2) + self.assertEqual(irc._foldr(operator.sub, 0, [1, 2, 3, 4]), -2) def insertTop(l, x): l.insert(0, x) return l self.assertEqual( - irc._foldr(insertTop, [], [[1], [2], [3], [4]]), - [[[[[], 4], 3], 2], 1]) - + irc._foldr(insertTop, [], [[1], [2], [3], [4]]), [[[[[], 4], 3], 2], 1] + ) class FormattedTextTests(IRCTestCase): """ Tests for parsing and assembling formatted IRC text. """ + def assertAssembledEqually(self, text, expectedFormatted): """ Assert that C{text} is parsed and assembled to the same value as what C{expectedFormatted} is assembled to. This provides a way to ignore meaningless differences in the formatting structure that would be difficult to detect without rendering the structures. """ formatted = irc.parseFormattedText(text) self.assertAssemblesTo(formatted, expectedFormatted) - def assertAssemblesTo(self, formatted, expectedFormatted): """ Assert that C{formatted} and C{expectedFormatted} assemble to the same value. """ text = irc.assembleFormattedText(formatted) expectedText = irc.assembleFormattedText(expectedFormatted) self.assertEqual( irc.assembleFormattedText(formatted), expectedText, - '%r (%r) is not equivalent to %r (%r)' % ( - text, formatted, expectedText, expectedFormatted)) - + "%r (%r) is not equivalent to %r (%r)" + % (text, formatted, expectedText, expectedFormatted), + ) def test_parseEmpty(self): """ An empty string parses to a I{normal} attribute with no text. """ - self.assertAssembledEqually('', A.normal) - + self.assertAssembledEqually("", A.normal) def test_assembleEmpty(self): """ An attribute with no text assembles to the empty string. An attribute whose text is the empty string assembles to two control codes: C{off} and that of the attribute. """ - self.assertEqual( - irc.assembleFormattedText(A.normal), - '') + self.assertEqual(irc.assembleFormattedText(A.normal), "") # Attempting to apply an attribute to the empty string should still # produce two control codes. - self.assertEqual( - irc.assembleFormattedText( - A.bold['']), - '\x0f\x02') - + self.assertEqual(irc.assembleFormattedText(A.bold[""]), "\x0f\x02") def test_assembleNormal(self): """ A I{normal} string assembles to a string prefixed with the I{off} control code. """ - self.assertEqual( - irc.assembleFormattedText( - A.normal['hello']), - '\x0fhello') - + self.assertEqual(irc.assembleFormattedText(A.normal["hello"]), "\x0fhello") def test_assembleBold(self): """ A I{bold} string assembles to a string prefixed with the I{off} and I{bold} control codes. """ - self.assertEqual( - irc.assembleFormattedText( - A.bold['hello']), - '\x0f\x02hello') - + self.assertEqual(irc.assembleFormattedText(A.bold["hello"]), "\x0f\x02hello") def test_assembleUnderline(self): """ An I{underline} string assembles to a string prefixed with the I{off} and I{underline} control codes. """ self.assertEqual( - irc.assembleFormattedText( - A.underline['hello']), - '\x0f\x1fhello') - + irc.assembleFormattedText(A.underline["hello"]), "\x0f\x1fhello" + ) def test_assembleReverseVideo(self): """ A I{reverse video} string assembles to a string prefixed with the I{off} and I{reverse video} control codes. """ self.assertEqual( - irc.assembleFormattedText( - A.reverseVideo['hello']), - '\x0f\x16hello') - + irc.assembleFormattedText(A.reverseVideo["hello"]), "\x0f\x16hello" + ) def test_assembleForegroundColor(self): """ A I{foreground color} string assembles to a string prefixed with the I{off} and I{color} (followed by the relevant foreground color code) control codes. """ self.assertEqual( - irc.assembleFormattedText( - A.fg.blue['hello']), - '\x0f\x0302hello') - + irc.assembleFormattedText(A.fg.blue["hello"]), "\x0f\x0302hello" + ) def test_assembleBackgroundColor(self): """ A I{background color} string assembles to a string prefixed with the I{off} and I{color} (followed by a I{,} to indicate the absence of a foreground color, followed by the relevant background color code) control codes. """ self.assertEqual( - irc.assembleFormattedText( - A.bg.blue['hello']), - '\x0f\x03,02hello') - + irc.assembleFormattedText(A.bg.blue["hello"]), "\x0f\x03,02hello" + ) def test_assembleColor(self): """ A I{foreground} and I{background} color string assembles to a string prefixed with the I{off} and I{color} (followed by the relevant foreground color, I{,} and the relevant background color code) control codes. """ self.assertEqual( - irc.assembleFormattedText( - A.fg.red[A.bg.blue['hello']]), - '\x0f\x0305,02hello') - + irc.assembleFormattedText(A.fg.red[A.bg.blue["hello"]]), + "\x0f\x0305,02hello", + ) def test_assembleNested(self): """ Nested attributes retain the attributes of their parents. """ self.assertEqual( - irc.assembleFormattedText( - A.bold['hello', A.underline[' world']]), - '\x0f\x02hello\x0f\x02\x1f world') + irc.assembleFormattedText(A.bold["hello", A.underline[" world"]]), + "\x0f\x02hello\x0f\x02\x1f world", + ) self.assertEqual( irc.assembleFormattedText( A.normal[ - A.fg.red[A.bg.green['hello'], ' world'], - A.reverseVideo[' yay']]), - '\x0f\x0305,03hello\x0f\x0305 world\x0f\x16 yay') - + A.fg.red[A.bg.green["hello"], " world"], A.reverseVideo[" yay"] + ] + ), + "\x0f\x0305,03hello\x0f\x0305 world\x0f\x16 yay", + ) def test_parseUnformattedText(self): """ Parsing unformatted text results in text with attributes that constitute a no-op. """ - self.assertEqual( - irc.parseFormattedText('hello'), - A.normal['hello']) - + self.assertEqual(irc.parseFormattedText("hello"), A.normal["hello"]) def test_colorFormatting(self): """ Correctly formatted text with colors uses 2 digits to specify foreground and (optionally) background. """ - self.assertEqual( - irc.parseFormattedText('\x0301yay\x03'), - A.fg.black['yay']) - self.assertEqual( - irc.parseFormattedText('\x0301,02yay\x03'), - A.fg.black[A.bg.blue['yay']]) - self.assertEqual( - irc.parseFormattedText('\x0301yay\x0302yipee\x03'), - A.fg.black['yay', A.fg.blue['yipee']]) - + self.assertEqual(irc.parseFormattedText("\x0301yay\x03"), A.fg.black["yay"]) + self.assertEqual( + irc.parseFormattedText("\x0301,02yay\x03"), A.fg.black[A.bg.blue["yay"]] + ) + self.assertEqual( + irc.parseFormattedText("\x0301yay\x0302yipee\x03"), + A.fg.black["yay", A.fg.blue["yipee"]], + ) def test_weirdColorFormatting(self): """ Formatted text with colors can use 1 digit for both foreground and background, as long as the text part does not begin with a digit. Foreground and background colors are only processed to a maximum of 2 digits per component, anything else is treated as text. Color sequences must begin with a digit, otherwise processing falls back to unformatted text. """ + self.assertAssembledEqually("\x031kinda valid", A.fg.black["kinda valid"]) self.assertAssembledEqually( - '\x031kinda valid', - A.fg.black['kinda valid']) + "\x03999,999kinda valid", A.fg.green["9,999kinda valid"] + ) self.assertAssembledEqually( - '\x03999,999kinda valid', - A.fg.green['9,999kinda valid']) + "\x031,2kinda valid", A.fg.black[A.bg.blue["kinda valid"]] + ) self.assertAssembledEqually( - '\x031,2kinda valid', - A.fg.black[A.bg.blue['kinda valid']]) + "\x031,999kinda valid", A.fg.black[A.bg.green["9kinda valid"]] + ) self.assertAssembledEqually( - '\x031,999kinda valid', - A.fg.black[A.bg.green['9kinda valid']]) - self.assertAssembledEqually( - '\x031,242 is a special number', - A.fg.black[A.bg.yellow['2 is a special number']]) - self.assertAssembledEqually( - '\x03,02oops\x03', - A.normal[',02oops']) - self.assertAssembledEqually( - '\x03wrong', - A.normal['wrong']) - self.assertAssembledEqually( - '\x031,hello', - A.fg.black['hello']) - self.assertAssembledEqually( - '\x03\x03', - A.normal) - + "\x031,242 is a special number", + A.fg.black[A.bg.yellow["2 is a special number"]], + ) + self.assertAssembledEqually("\x03,02oops\x03", A.normal[",02oops"]) + self.assertAssembledEqually("\x03wrong", A.normal["wrong"]) + self.assertAssembledEqually("\x031,hello", A.fg.black["hello"]) + self.assertAssembledEqually("\x03\x03", A.normal) def test_clearColorFormatting(self): """ An empty color format specifier clears foreground and background colors. """ self.assertAssembledEqually( - '\x0301yay\x03reset', - A.normal[A.fg.black['yay'], 'reset']) + "\x0301yay\x03reset", A.normal[A.fg.black["yay"], "reset"] + ) self.assertAssembledEqually( - '\x0301,02yay\x03reset', - A.normal[A.fg.black[A.bg.blue['yay']], 'reset']) - + "\x0301,02yay\x03reset", A.normal[A.fg.black[A.bg.blue["yay"]], "reset"] + ) def test_resetFormatting(self): """ A reset format specifier clears all formatting attributes. """ self.assertAssembledEqually( - '\x02\x1fyay\x0freset', - A.normal[A.bold[A.underline['yay']], 'reset']) + "\x02\x1fyay\x0freset", A.normal[A.bold[A.underline["yay"]], "reset"] + ) self.assertAssembledEqually( - '\x0301yay\x0freset', - A.normal[A.fg.black['yay'], 'reset']) + "\x0301yay\x0freset", A.normal[A.fg.black["yay"], "reset"] + ) self.assertAssembledEqually( - '\x0301,02yay\x0freset', - A.normal[A.fg.black[A.bg.blue['yay']], 'reset']) - + "\x0301,02yay\x0freset", A.normal[A.fg.black[A.bg.blue["yay"]], "reset"] + ) def test_stripFormatting(self): """ Strip formatting codes from formatted text, leaving only the text parts. """ self.assertEqual( irc.stripFormatting( irc.assembleFormattedText( A.bold[ A.underline[ - A.reverseVideo[A.fg.red[A.bg.green['hello']]], - ' world']])), - 'hello world') - + A.reverseVideo[A.fg.red[A.bg.green["hello"]]], " world" + ] + ] + ) + ), + "hello world", + ) class FormattingStateAttributeTests(IRCTestCase): """ Tests for L{twisted.words.protocols.irc._FormattingState}. """ + def test_equality(self): """ L{irc._FormattingState}s must have matching character attribute values (bold, underline, etc) with the same values to be considered equal. """ - self.assertEqual( - irc._FormattingState(), - irc._FormattingState()) - - self.assertEqual( - irc._FormattingState(), - irc._FormattingState(off=False)) + self.assertEqual(irc._FormattingState(), irc._FormattingState()) + + self.assertEqual(irc._FormattingState(), irc._FormattingState(off=False)) self.assertEqual( irc._FormattingState( - bold=True, underline=True, off=False, reverseVideo=True, - foreground=irc._IRC_COLORS['blue']), + bold=True, + underline=True, + off=False, + reverseVideo=True, + foreground=irc._IRC_COLORS["blue"], + ), irc._FormattingState( - bold=True, underline=True, off=False, reverseVideo=True, - foreground=irc._IRC_COLORS['blue'])) + bold=True, + underline=True, + off=False, + reverseVideo=True, + foreground=irc._IRC_COLORS["blue"], + ), + ) self.assertNotEqual( - irc._FormattingState(bold=True), - irc._FormattingState(bold=False)) - + irc._FormattingState(bold=True), irc._FormattingState(bold=False) + ) stringSubjects = [ "Hello, this is a nice string with no complications.", - "xargs%(NUL)smight%(NUL)slike%(NUL)sthis" % {'NUL': irc.NUL }, - "embedded%(CR)snewline%(CR)s%(NL)sFUN%(NL)s" % {'CR': irc.CR, - 'NL': irc.NL}, - "escape!%(X)s escape!%(M)s %(X)s%(X)sa %(M)s0" % {'X': irc.X_QUOTE, - 'M': irc.M_QUOTE} - ] + "xargs%(NUL)smight%(NUL)slike%(NUL)sthis" % {"NUL": irc.NUL}, + "embedded%(CR)snewline%(CR)s%(NL)sFUN%(NL)s" % {"CR": irc.CR, "NL": irc.NL}, + "escape!%(X)s escape!%(M)s %(X)s%(X)sa %(M)s0" + % {"X": irc.X_QUOTE, "M": irc.M_QUOTE}, +] class QuotingTests(IRCTestCase): def test_lowquoteSanity(self): """ Testing client-server level quote/dequote. """ for s in stringSubjects: self.assertEqual(s, irc.lowDequote(irc.lowQuote(s))) - def test_ctcpquoteSanity(self): """ Testing CTCP message level quote/dequote. """ for s in stringSubjects: self.assertEqual(s, irc.ctcpDequote(irc.ctcpQuote(s))) - class Dispatcher(irc._CommandDispatcherMixin): """ A dispatcher that exposes one known command and handles unknown commands. """ - prefix = 'disp' + + prefix = "disp" def disp_working(self, a, b): """ A known command that returns its input. """ return a, b - def disp_unknown(self, name, a, b): """ Handle unknown commands by returning their name and inputs. """ return name, a, b - class DispatcherTests(IRCTestCase): """ Tests for L{irc._CommandDispatcherMixin}. """ + def test_dispatch(self): """ Dispatching a command invokes the correct handler. """ disp = Dispatcher() args = (1, 2) - res = disp.dispatch('working', *args) + res = disp.dispatch("working", *args) self.assertEqual(res, args) - def test_dispatchUnknown(self): """ Dispatching an unknown command invokes the default handler. """ disp = Dispatcher() - name = 'missing' + name = "missing" args = (1, 2) res = disp.dispatch(name, *args) self.assertEqual(res, (name,) + args) - def test_dispatchMissingUnknown(self): """ Dispatching an unknown command, when no default handler is present, results in an exception being raised. """ disp = Dispatcher() disp.disp_unknown = None - self.assertRaises(irc.UnhandledCommand, disp.dispatch, 'bar') - + self.assertRaises(irc.UnhandledCommand, disp.dispatch, "bar") class ServerSupportedFeatureTests(IRCTestCase): """ Tests for L{ServerSupportedFeatures} and related functions. """ + def test_intOrDefault(self): """ L{_intOrDefault} converts values to C{int} if possible, otherwise returns a default value. """ self.assertEqual(irc._intOrDefault(None), None) self.assertEqual(irc._intOrDefault([]), None) - self.assertEqual(irc._intOrDefault(''), None) - self.assertEqual(irc._intOrDefault('hello', 5), 5) - self.assertEqual(irc._intOrDefault('123'), 123) + self.assertEqual(irc._intOrDefault(""), None) + self.assertEqual(irc._intOrDefault("hello", 5), 5) + self.assertEqual(irc._intOrDefault("123"), 123) self.assertEqual(irc._intOrDefault(123), 123) - def test_splitParam(self): """ L{ServerSupportedFeatures._splitParam} splits ISUPPORT parameters into key and values. Parameters without a separator are split into a key and a list containing only the empty string. Escaped parameters are unescaped. """ - params = [('FOO', ('FOO', [''])), - ('FOO=', ('FOO', [''])), - ('FOO=1', ('FOO', ['1'])), - ('FOO=1,2,3', ('FOO', ['1', '2', '3'])), - ('FOO=A\\x20B', ('FOO', ['A B'])), - ('FOO=\\x5Cx', ('FOO', ['\\x'])), - ('FOO=\\', ('FOO', ['\\'])), - ('FOO=\\n', ('FOO', ['\\n']))] + params = [ + ("FOO", ("FOO", [""])), + ("FOO=", ("FOO", [""])), + ("FOO=1", ("FOO", ["1"])), + ("FOO=1,2,3", ("FOO", ["1", "2", "3"])), + ("FOO=A\\x20B", ("FOO", ["A B"])), + ("FOO=\\x5Cx", ("FOO", ["\\x"])), + ("FOO=\\", ("FOO", ["\\"])), + ("FOO=\\n", ("FOO", ["\\n"])), + ] _splitParam = irc.ServerSupportedFeatures._splitParam for param, expected in params: res = _splitParam(param) self.assertEqual(res, expected) - self.assertRaises(ValueError, _splitParam, 'FOO=\\x') - self.assertRaises(ValueError, _splitParam, 'FOO=\\xNN') - self.assertRaises(ValueError, _splitParam, 'FOO=\\xN') - self.assertRaises(ValueError, _splitParam, 'FOO=\\x20\\x') - + self.assertRaises(ValueError, _splitParam, "FOO=\\x") + self.assertRaises(ValueError, _splitParam, "FOO=\\xNN") + self.assertRaises(ValueError, _splitParam, "FOO=\\xN") + self.assertRaises(ValueError, _splitParam, "FOO=\\x20\\x") def test_splitParamArgs(self): """ L{ServerSupportedFeatures._splitParamArgs} splits ISUPPORT parameter arguments into key and value. Arguments without a separator are split into a key and an empty string. """ - res = irc.ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2', 'C:', 'D']) - self.assertEqual(res, [('A', '1'), - ('B', '2'), - ('C', ''), - ('D', '')]) - + res = irc.ServerSupportedFeatures._splitParamArgs(["A:1", "B:2", "C:", "D"]) + self.assertEqual(res, [("A", "1"), ("B", "2"), ("C", ""), ("D", "")]) def test_splitParamArgsProcessor(self): """ L{ServerSupportedFeatures._splitParamArgs} uses the argument processor passed to convert ISUPPORT argument values to some more suitable form. """ - res = irc.ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2', 'C'], - irc._intOrDefault) - self.assertEqual(res, [('A', 1), - ('B', 2), - ('C', None)]) - + res = irc.ServerSupportedFeatures._splitParamArgs( + ["A:1", "B:2", "C"], irc._intOrDefault + ) + self.assertEqual(res, [("A", 1), ("B", 2), ("C", None)]) def test_parsePrefixParam(self): """ L{ServerSupportedFeatures._parsePrefixParam} parses the ISUPPORT PREFIX parameter into a mapping from modes to prefix symbols, returns L{None} if there is no parseable prefix parameter or raises C{ValueError} if the prefix parameter is malformed. """ _parsePrefixParam = irc.ServerSupportedFeatures._parsePrefixParam - self.assertEqual(_parsePrefixParam(''), None) - self.assertRaises(ValueError, _parsePrefixParam, 'hello') - self.assertEqual(_parsePrefixParam('(ov)@+'), - {'o': ('@', 0), - 'v': ('+', 1)}) - + self.assertEqual(_parsePrefixParam(""), None) + self.assertRaises(ValueError, _parsePrefixParam, "hello") + self.assertEqual(_parsePrefixParam("(ov)@+"), {"o": ("@", 0), "v": ("+", 1)}) def test_parseChanModesParam(self): """ L{ServerSupportedFeatures._parseChanModesParam} parses the ISUPPORT CHANMODES parameter into a mapping from mode categories to mode @@ -689,63 +629,49 @@ for the relevant categories. Passing more than 4 parameters raises C{ValueError}. """ _parseChanModesParam = irc.ServerSupportedFeatures._parseChanModesParam self.assertEqual( - _parseChanModesParam(['', '', '', '']), - {'addressModes': '', - 'param': '', - 'setParam': '', - 'noParam': ''}) - - self.assertEqual( - _parseChanModesParam(['b', 'k', 'l', 'imnpst']), - {'addressModes': 'b', - 'param': 'k', - 'setParam': 'l', - 'noParam': 'imnpst'}) - - self.assertEqual( - _parseChanModesParam(['b', 'k', 'l', '']), - {'addressModes': 'b', - 'param': 'k', - 'setParam': 'l', - 'noParam': ''}) - - self.assertRaises( - ValueError, - _parseChanModesParam, ['a', 'b', 'c', 'd', 'e']) - + _parseChanModesParam(["", "", "", ""]), + {"addressModes": "", "param": "", "setParam": "", "noParam": ""}, + ) + + self.assertEqual( + _parseChanModesParam(["b", "k", "l", "imnpst"]), + {"addressModes": "b", "param": "k", "setParam": "l", "noParam": "imnpst"}, + ) + + self.assertEqual( + _parseChanModesParam(["b", "k", "l", ""]), + {"addressModes": "b", "param": "k", "setParam": "l", "noParam": ""}, + ) + + self.assertRaises(ValueError, _parseChanModesParam, ["a", "b", "c", "d", "e"]) def test_parse(self): """ L{ServerSupportedFeatures.parse} changes the internal state of the instance to reflect the features indicated by the parsed ISUPPORT parameters, including unknown parameters and unsetting previously set parameters. """ supported = irc.ServerSupportedFeatures() - supported.parse(['MODES=4', - 'CHANLIMIT=#:20,&:10', - 'INVEX', - 'EXCEPTS=Z', - 'UNKNOWN=A,B,C']) - - self.assertEqual(supported.getFeature('MODES'), 4) - self.assertEqual(supported.getFeature('CHANLIMIT'), - [('#', 20), - ('&', 10)]) - self.assertEqual(supported.getFeature('INVEX'), 'I') - self.assertEqual(supported.getFeature('EXCEPTS'), 'Z') - self.assertEqual(supported.getFeature('UNKNOWN'), ('A', 'B', 'C')) - - self.assertTrue(supported.hasFeature('INVEX')) - supported.parse(['-INVEX']) - self.assertFalse(supported.hasFeature('INVEX')) + supported.parse( + ["MODES=4", "CHANLIMIT=#:20,&:10", "INVEX", "EXCEPTS=Z", "UNKNOWN=A,B,C"] + ) + + self.assertEqual(supported.getFeature("MODES"), 4) + self.assertEqual(supported.getFeature("CHANLIMIT"), [("#", 20), ("&", 10)]) + self.assertEqual(supported.getFeature("INVEX"), "I") + self.assertEqual(supported.getFeature("EXCEPTS"), "Z") + self.assertEqual(supported.getFeature("UNKNOWN"), ("A", "B", "C")) + + self.assertTrue(supported.hasFeature("INVEX")) + supported.parse(["-INVEX"]) + self.assertFalse(supported.hasFeature("INVEX")) # Unsetting a previously unset parameter should not be a problem. - supported.parse(['-INVEX']) - + supported.parse(["-INVEX"]) def _parse(self, features): """ Parse all specified features according to the ISUPPORT specifications. @@ -753,340 +679,283 @@ @param features: Feature names and values to parse @rtype: L{irc.ServerSupportedFeatures} """ supported = irc.ServerSupportedFeatures() - features = ['%s=%s' % (name, value or '') - for name, value in features] + features = ["%s=%s" % (name, value or "") for name, value in features] supported.parse(features) return supported - def _parseFeature(self, name, value=None): """ Parse a feature, with the given name and value, according to the ISUPPORT specifications and return the parsed value. """ supported = self._parse([(name, value)]) return supported.getFeature(name) - def _testIntOrDefaultFeature(self, name, default=None): """ Perform some common tests on a feature known to use L{_intOrDefault}. """ - self.assertEqual( - self._parseFeature(name, None), - default) - self.assertEqual( - self._parseFeature(name, 'notanint'), - default) - self.assertEqual( - self._parseFeature(name, '42'), - 42) - + self.assertEqual(self._parseFeature(name, None), default) + self.assertEqual(self._parseFeature(name, "notanint"), default) + self.assertEqual(self._parseFeature(name, "42"), 42) def _testFeatureDefault(self, name, features=None): """ Features known to have default values are reported as being present by L{irc.ServerSupportedFeatures.hasFeature}, and their value defaults correctly, when they don't appear in an ISUPPORT message. """ default = irc.ServerSupportedFeatures()._features[name] if features is None: - features = [('DEFINITELY_NOT', 'a_feature')] + features = [("DEFINITELY_NOT", "a_feature")] supported = self._parse(features) self.assertTrue(supported.hasFeature(name)) self.assertEqual(supported.getFeature(name), default) - def test_support_CHANMODES(self): """ The CHANMODES ISUPPORT parameter is parsed into a C{dict} giving the four mode categories, C{'addressModes'}, C{'param'}, C{'setParam'}, and C{'noParam'}. """ - self._testFeatureDefault('CHANMODES') - self._testFeatureDefault('CHANMODES', [('CHANMODES', 'b,,lk,')]) - self._testFeatureDefault('CHANMODES', [('CHANMODES', 'b,,lk,ha,ha')]) - - self.assertEqual( - self._parseFeature('CHANMODES', ',,,'), - {'addressModes': '', - 'param': '', - 'setParam': '', - 'noParam': ''}) - - self.assertEqual( - self._parseFeature('CHANMODES', ',A,,'), - {'addressModes': '', - 'param': 'A', - 'setParam': '', - 'noParam': ''}) - - self.assertEqual( - self._parseFeature('CHANMODES', 'A,Bc,Def,Ghij'), - {'addressModes': 'A', - 'param': 'Bc', - 'setParam': 'Def', - 'noParam': 'Ghij'}) - + self._testFeatureDefault("CHANMODES") + self._testFeatureDefault("CHANMODES", [("CHANMODES", "b,,lk,")]) + self._testFeatureDefault("CHANMODES", [("CHANMODES", "b,,lk,ha,ha")]) + + self.assertEqual( + self._parseFeature("CHANMODES", ",,,"), + {"addressModes": "", "param": "", "setParam": "", "noParam": ""}, + ) + + self.assertEqual( + self._parseFeature("CHANMODES", ",A,,"), + {"addressModes": "", "param": "A", "setParam": "", "noParam": ""}, + ) + + self.assertEqual( + self._parseFeature("CHANMODES", "A,Bc,Def,Ghij"), + {"addressModes": "A", "param": "Bc", "setParam": "Def", "noParam": "Ghij"}, + ) def test_support_IDCHAN(self): """ The IDCHAN support parameter is parsed into a sequence of two-tuples giving channel prefix and ID length pairs. """ - self.assertEqual( - self._parseFeature('IDCHAN', '!:5'), - [('!', '5')]) - + self.assertEqual(self._parseFeature("IDCHAN", "!:5"), [("!", "5")]) def test_support_MAXLIST(self): """ The MAXLIST support parameter is parsed into a sequence of two-tuples giving modes and their limits. """ self.assertEqual( - self._parseFeature('MAXLIST', 'b:25,eI:50'), - [('b', 25), ('eI', 50)]) + self._parseFeature("MAXLIST", "b:25,eI:50"), [("b", 25), ("eI", 50)] + ) # A non-integer parameter argument results in None. self.assertEqual( - self._parseFeature('MAXLIST', 'b:25,eI:50,a:3.1415'), - [('b', 25), ('eI', 50), ('a', None)]) - self.assertEqual( - self._parseFeature('MAXLIST', 'b:25,eI:50,a:notanint'), - [('b', 25), ('eI', 50), ('a', None)]) - + self._parseFeature("MAXLIST", "b:25,eI:50,a:3.1415"), + [("b", 25), ("eI", 50), ("a", None)], + ) + self.assertEqual( + self._parseFeature("MAXLIST", "b:25,eI:50,a:notanint"), + [("b", 25), ("eI", 50), ("a", None)], + ) def test_support_NETWORK(self): """ The NETWORK support parameter is parsed as the network name, as specified by the server. """ - self.assertEqual( - self._parseFeature('NETWORK', 'IRCNet'), - 'IRCNet') - + self.assertEqual(self._parseFeature("NETWORK", "IRCNet"), "IRCNet") def test_support_SAFELIST(self): """ The SAFELIST support parameter is parsed into a boolean indicating whether the safe "list" command is supported or not. """ - self.assertEqual( - self._parseFeature('SAFELIST'), - True) - + self.assertEqual(self._parseFeature("SAFELIST"), True) def test_support_STATUSMSG(self): """ The STATUSMSG support parameter is parsed into a string of channel status that support the exclusive channel notice method. """ - self.assertEqual( - self._parseFeature('STATUSMSG', '@+'), - '@+') - + self.assertEqual(self._parseFeature("STATUSMSG", "@+"), "@+") def test_support_TARGMAX(self): """ The TARGMAX support parameter is parsed into a dictionary, mapping strings to integers, of the maximum number of targets for a particular command. """ self.assertEqual( - self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3'), - {'PRIVMSG': 4, - 'NOTICE': 3}) + self._parseFeature("TARGMAX", "PRIVMSG:4,NOTICE:3"), + {"PRIVMSG": 4, "NOTICE": 3}, + ) # A non-integer parameter argument results in None. self.assertEqual( - self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3,KICK:3.1415'), - {'PRIVMSG': 4, - 'NOTICE': 3, - 'KICK': None}) - self.assertEqual( - self._parseFeature('TARGMAX', 'PRIVMSG:4,NOTICE:3,KICK:notanint'), - {'PRIVMSG': 4, - 'NOTICE': 3, - 'KICK': None}) - + self._parseFeature("TARGMAX", "PRIVMSG:4,NOTICE:3,KICK:3.1415"), + {"PRIVMSG": 4, "NOTICE": 3, "KICK": None}, + ) + self.assertEqual( + self._parseFeature("TARGMAX", "PRIVMSG:4,NOTICE:3,KICK:notanint"), + {"PRIVMSG": 4, "NOTICE": 3, "KICK": None}, + ) def test_support_NICKLEN(self): """ The NICKLEN support parameter is parsed into an integer value indicating the maximum length of a nickname the client may use, otherwise, if the parameter is missing or invalid, the default value (as specified by RFC 1459) is used. """ - default = irc.ServerSupportedFeatures()._features['NICKLEN'] - self._testIntOrDefaultFeature('NICKLEN', default) - + default = irc.ServerSupportedFeatures()._features["NICKLEN"] + self._testIntOrDefaultFeature("NICKLEN", default) def test_support_CHANNELLEN(self): """ The CHANNELLEN support parameter is parsed into an integer value indicating the maximum channel name length, otherwise, if the parameter is missing or invalid, the default value (as specified by RFC 1459) is used. """ - default = irc.ServerSupportedFeatures()._features['CHANNELLEN'] - self._testIntOrDefaultFeature('CHANNELLEN', default) - + default = irc.ServerSupportedFeatures()._features["CHANNELLEN"] + self._testIntOrDefaultFeature("CHANNELLEN", default) def test_support_CHANTYPES(self): """ The CHANTYPES support parameter is parsed into a tuple of valid channel prefix characters. """ - self._testFeatureDefault('CHANTYPES') - - self.assertEqual( - self._parseFeature('CHANTYPES', '#&%'), - ('#', '&', '%')) - + self._testFeatureDefault("CHANTYPES") + + self.assertEqual(self._parseFeature("CHANTYPES", "#&%"), ("#", "&", "%")) def test_support_KICKLEN(self): """ The KICKLEN support parameter is parsed into an integer value indicating the maximum length of a kick message a client may use. """ - self._testIntOrDefaultFeature('KICKLEN') - + self._testIntOrDefaultFeature("KICKLEN") def test_support_PREFIX(self): """ The PREFIX support parameter is parsed into a dictionary mapping modes to two-tuples of status symbol and priority. """ - self._testFeatureDefault('PREFIX') - self._testFeatureDefault('PREFIX', [('PREFIX', 'hello')]) - - self.assertEqual( - self._parseFeature('PREFIX', None), - None) - self.assertEqual( - self._parseFeature('PREFIX', '(ohv)@%+'), - {'o': ('@', 0), - 'h': ('%', 1), - 'v': ('+', 2)}) - self.assertEqual( - self._parseFeature('PREFIX', '(hov)@%+'), - {'o': ('%', 1), - 'h': ('@', 0), - 'v': ('+', 2)}) - + self._testFeatureDefault("PREFIX") + self._testFeatureDefault("PREFIX", [("PREFIX", "hello")]) + + self.assertEqual(self._parseFeature("PREFIX", None), None) + self.assertEqual( + self._parseFeature("PREFIX", "(ohv)@%+"), + {"o": ("@", 0), "h": ("%", 1), "v": ("+", 2)}, + ) + self.assertEqual( + self._parseFeature("PREFIX", "(hov)@%+"), + {"o": ("%", 1), "h": ("@", 0), "v": ("+", 2)}, + ) def test_support_TOPICLEN(self): """ The TOPICLEN support parameter is parsed into an integer value indicating the maximum length of a topic a client may set. """ - self._testIntOrDefaultFeature('TOPICLEN') - + self._testIntOrDefaultFeature("TOPICLEN") def test_support_MODES(self): """ The MODES support parameter is parsed into an integer value indicating the maximum number of "variable" modes (defined as being modes from C{addressModes}, C{param} or C{setParam} categories for the C{CHANMODES} ISUPPORT parameter) which may by set on a channel by a single MODE command from a client. """ - self._testIntOrDefaultFeature('MODES') - + self._testIntOrDefaultFeature("MODES") def test_support_EXCEPTS(self): """ The EXCEPTS support parameter is parsed into the mode character to be used for "ban exception" modes. If no parameter is specified then the character C{e} is assumed. """ - self.assertEqual( - self._parseFeature('EXCEPTS', 'Z'), - 'Z') - self.assertEqual( - self._parseFeature('EXCEPTS'), - 'e') - + self.assertEqual(self._parseFeature("EXCEPTS", "Z"), "Z") + self.assertEqual(self._parseFeature("EXCEPTS"), "e") def test_support_INVEX(self): """ The INVEX support parameter is parsed into the mode character to be used for "invite exception" modes. If no parameter is specified then the character C{I} is assumed. """ - self.assertEqual( - self._parseFeature('INVEX', 'Z'), - 'Z') - self.assertEqual( - self._parseFeature('INVEX'), - 'I') - + self.assertEqual(self._parseFeature("INVEX", "Z"), "Z") + self.assertEqual(self._parseFeature("INVEX"), "I") class IRCClientWithoutLogin(irc.IRCClient): performLogin = 0 - class CTCPTests(IRCTestCase): """ Tests for L{twisted.words.protocols.irc.IRCClient} CTCP handling. """ + def setUp(self): self.file = StringIOWithoutClosing() self.transport = protocol.FileWrapper(self.file) self.client = IRCClientWithoutLogin() self.client.makeConnection(self.transport) self.addCleanup(self.transport.loseConnection) self.addCleanup(self.client.connectionLost, None) - def test_ERRMSG(self): """Testing CTCP query ERRMSG. Not because this is this is an especially important case in the field, but it does go through the entire dispatch/decode/encode process. """ - errQuery = (":nick!guy@over.there PRIVMSG #theChan :" - "%(X)cERRMSG t%(X)c%(EOL)s" - % {'X': irc.X_DELIM, - 'EOL': irc.CR + irc.LF}) - - errReply = ("NOTICE nick :%(X)cERRMSG t :" - "No error has occurred.%(X)c%(EOL)s" - % {'X': irc.X_DELIM, - 'EOL': irc.CR + irc.LF}) + errQuery = ( + ":nick!guy@over.there PRIVMSG #theChan :" + "%(X)cERRMSG t%(X)c%(EOL)s" % {"X": irc.X_DELIM, "EOL": irc.CR + irc.LF} + ) + + errReply = ( + "NOTICE nick :%(X)cERRMSG t :" + "No error has occurred.%(X)c%(EOL)s" + % {"X": irc.X_DELIM, "EOL": irc.CR + irc.LF} + ) self.client.dataReceived(errQuery) reply = self.file.getvalue() self.assertEqualBufferValue(reply, errReply) - def test_noNumbersVERSION(self): """ If attributes for version information on L{IRCClient} are set to L{None}, the parts of the CTCP VERSION response they correspond to are omitted. """ self.client.versionName = "FrobozzIRC" self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None) - versionReply = ("NOTICE nick :%(X)cVERSION %(vname)s::" - "%(X)c%(EOL)s" - % {'X': irc.X_DELIM, - 'EOL': irc.CR + irc.LF, - 'vname': self.client.versionName}) + versionReply = "NOTICE nick :%(X)cVERSION %(vname)s::" "%(X)c%(EOL)s" % { + "X": irc.X_DELIM, + "EOL": irc.CR + irc.LF, + "vname": self.client.versionName, + } reply = self.file.getvalue() self.assertEqualBufferValue(reply, versionReply) - def test_fullVERSION(self): """ The response to a CTCP VERSION query includes the version number and environment information, as specified by L{IRCClient.versionNum} and @@ -1094,135 +963,140 @@ """ self.client.versionName = "FrobozzIRC" self.client.versionNum = "1.2g" self.client.versionEnv = "ZorkOS" self.client.ctcpQuery_VERSION("nick!guy@over.there", "#theChan", None) - versionReply = ("NOTICE nick :%(X)cVERSION %(vname)s:%(vnum)s:%(venv)s" - "%(X)c%(EOL)s" - % {'X': irc.X_DELIM, - 'EOL': irc.CR + irc.LF, - 'vname': self.client.versionName, - 'vnum': self.client.versionNum, - 'venv': self.client.versionEnv}) + versionReply = ( + "NOTICE nick :%(X)cVERSION %(vname)s:%(vnum)s:%(venv)s" + "%(X)c%(EOL)s" + % { + "X": irc.X_DELIM, + "EOL": irc.CR + irc.LF, + "vname": self.client.versionName, + "vnum": self.client.versionNum, + "venv": self.client.versionEnv, + } + ) reply = self.file.getvalue() self.assertEqualBufferValue(reply, versionReply) - def test_noDuplicateCTCPDispatch(self): """ Duplicated CTCP messages are ignored and no reply is made. """ + def testCTCP(user, channel, data): self.called += 1 self.called = 0 self.client.ctcpQuery_TESTTHIS = testCTCP self.client.irc_PRIVMSG( - 'foo!bar@baz.quux', [ - '#chan', - '%(X)sTESTTHIS%(X)sfoo%(X)sTESTTHIS%(X)s' % {'X': irc.X_DELIM}]) - self.assertEqualBufferValue(self.file.getvalue(), '') + "foo!bar@baz.quux", + ["#chan", "%(X)sTESTTHIS%(X)sfoo%(X)sTESTTHIS%(X)s" % {"X": irc.X_DELIM}], + ) + self.assertEqualBufferValue(self.file.getvalue(), "") self.assertEqual(self.called, 1) - def test_noDefaultDispatch(self): """ The fallback handler is invoked for unrecognized CTCP messages. """ + def unknownQuery(user, channel, tag, data): self.calledWith = (user, channel, tag, data) self.called += 1 self.called = 0 - self.patch(self.client, 'ctcpUnknownQuery', unknownQuery) + self.patch(self.client, "ctcpUnknownQuery", unknownQuery) self.client.irc_PRIVMSG( - 'foo!bar@baz.quux', [ - '#chan', - '%(X)sNOTREAL%(X)s' % {'X': irc.X_DELIM}]) - self.assertEqualBufferValue(self.file.getvalue(), '') - self.assertEqual( - self.calledWith, - ('foo!bar@baz.quux', '#chan', 'NOTREAL', None)) + "foo!bar@baz.quux", ["#chan", "%(X)sNOTREAL%(X)s" % {"X": irc.X_DELIM}] + ) + self.assertEqualBufferValue(self.file.getvalue(), "") + self.assertEqual( + self.calledWith, ("foo!bar@baz.quux", "#chan", "NOTREAL", None) + ) self.assertEqual(self.called, 1) # The fallback handler is not invoked for duplicate unknown CTCP # messages. self.client.irc_PRIVMSG( - 'foo!bar@baz.quux', [ - '#chan', - '%(X)sNOTREAL%(X)sfoo%(X)sNOTREAL%(X)s' % {'X': irc.X_DELIM}]) + "foo!bar@baz.quux", + ["#chan", "%(X)sNOTREAL%(X)sfoo%(X)sNOTREAL%(X)s" % {"X": irc.X_DELIM}], + ) self.assertEqual(self.called, 2) - class NoticingClient(IRCClientWithoutLogin): methods = { - 'created': ('when',), - 'yourHost': ('info',), - 'myInfo': ('servername', 'version', 'umodes', 'cmodes'), - 'luserClient': ('info',), - 'bounce': ('info',), - 'isupport': ('options',), - 'luserChannels': ('channels',), - 'luserOp': ('ops',), - 'luserMe': ('info',), - 'receivedMOTD': ('motd',), - - 'privmsg': ('user', 'channel', 'message'), - 'joined': ('channel',), - 'left': ('channel',), - 'noticed': ('user', 'channel', 'message'), - 'modeChanged': ('user', 'channel', 'set', 'modes', 'args'), - 'pong': ('user', 'secs'), - 'signedOn': (), - 'kickedFrom': ('channel', 'kicker', 'message'), - 'nickChanged': ('nick',), - - 'userJoined': ('user', 'channel'), - 'userLeft': ('user', 'channel'), - 'userKicked': ('user', 'channel', 'kicker', 'message'), - 'action': ('user', 'channel', 'data'), - 'topicUpdated': ('user', 'channel', 'newTopic'), - 'userRenamed': ('oldname', 'newname')} - + "created": ("when",), + "yourHost": ("info",), + "myInfo": ("servername", "version", "umodes", "cmodes"), + "luserClient": ("info",), + "bounce": ("info",), + "isupport": ("options",), + "luserChannels": ("channels",), + "luserOp": ("ops",), + "luserMe": ("info",), + "receivedMOTD": ("motd",), + "privmsg": ("user", "channel", "message"), + "joined": ("channel",), + "left": ("channel",), + "noticed": ("user", "channel", "message"), + "modeChanged": ("user", "channel", "set", "modes", "args"), + "pong": ("user", "secs"), + "signedOn": (), + "kickedFrom": ("channel", "kicker", "message"), + "nickChanged": ("nick",), + "userJoined": ("user", "channel"), + "userLeft": ("user", "channel"), + "userKicked": ("user", "channel", "kicker", "message"), + "action": ("user", "channel", "data"), + "topicUpdated": ("user", "channel", "newTopic"), + "userRenamed": ("oldname", "newname"), + } def __init__(self, *a, **kw): # It is important that IRCClient.__init__ is not called since # traditionally it did not exist, so it is important that nothing is # initialised there that would prevent subclasses that did not (or # could not) invoke the base implementation. Any protocol # initialisation should happen in connectionMode. self.calls = [] - def __getattribute__(self, name): - if name.startswith('__') and name.endswith('__'): + if name.startswith("__") and name.endswith("__"): return super(NoticingClient, self).__getattribute__(name) try: - args = super(NoticingClient, self).__getattribute__('methods')[name] + args = super(NoticingClient, self).__getattribute__("methods")[name] except KeyError: return super(NoticingClient, self).__getattribute__(name) else: return self.makeMethod(name, args) - def makeMethod(self, fname, args): def method(*a, **kw): if len(a) > len(args): - raise TypeError("TypeError: %s() takes %d arguments " - "(%d given)" % (fname, len(args), len(a))) + raise TypeError( + "TypeError: %s() takes %d arguments " + "(%d given)" % (fname, len(args), len(a)) + ) for (name, value) in zip(args, a): if name in kw: - raise TypeError("TypeError: %s() got multiple values " - "for keyword argument '%s'" % (fname, name)) + raise TypeError( + "TypeError: %s() got multiple values " + "for keyword argument '%s'" % (fname, name) + ) else: kw[name] = value if len(kw) != len(args): - raise TypeError("TypeError: %s() takes %d arguments " - "(%d given)" % (fname, len(args), len(a))) + raise TypeError( + "TypeError: %s() takes %d arguments " + "(%d given)" % (fname, len(args), len(a)) + ) self.calls.append((fname, kw)) + return method def pop(dict, key, default): try: @@ -1232,231 +1106,234 @@ else: del dict[key] return value - class ClientImplementationTests(IRCTestCase): def setUp(self): self.transport = StringTransport() self.client = NoticingClient() self.client.makeConnection(self.transport) self.addCleanup(self.transport.loseConnection) self.addCleanup(self.client.connectionLost, None) - def _serverTestImpl(self, code, msg, func, **kw): - host = pop(kw, 'host', 'server.host') - nick = pop(kw, 'nick', 'nickname') - args = pop(kw, 'args', '') - - message = (":" + - host + " " + - code + " " + - nick + " " + - args + " :" + - msg + "\r\n") + host = pop(kw, "host", "server.host") + nick = pop(kw, "nick", "nickname") + args = pop(kw, "args", "") + + message = ( + ":" + host + " " + code + " " + nick + " " + args + " :" + msg + "\r\n" + ) self.client.dataReceived(message) - self.assertEqual( - self.client.calls, - [(func, kw)]) - + self.assertEqual(self.client.calls, [(func, kw)]) def testYourHost(self): msg = "Your host is some.host[blah.blah/6667], running version server-version-3" self._serverTestImpl("002", msg, "yourHost", info=msg) - def testCreated(self): msg = "This server was cobbled together Fri Aug 13 18:00:25 UTC 2004" self._serverTestImpl("003", msg, "created", when=msg) - def testMyInfo(self): msg = "server.host server-version abcDEF bcdEHI" - self._serverTestImpl("004", msg, "myInfo", - servername="server.host", - version="server-version", - umodes="abcDEF", - cmodes="bcdEHI") - + self._serverTestImpl( + "004", + msg, + "myInfo", + servername="server.host", + version="server-version", + umodes="abcDEF", + cmodes="bcdEHI", + ) def testLuserClient(self): msg = "There are 9227 victims and 9542 hiding on 24 servers" - self._serverTestImpl("251", msg, "luserClient", - info=msg) - + self._serverTestImpl("251", msg, "luserClient", info=msg) def _sendISUPPORT(self): - args = ("MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63 " - "TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=# " - "PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer") + args = ( + "MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63 " + "TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=# " + "PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer" + ) msg = "are available on this server" - self._serverTestImpl("005", msg, "isupport", args=args, - options=['MODES=4', - 'CHANLIMIT=#:20', - 'NICKLEN=16', - 'USERLEN=10', - 'HOSTLEN=63', - 'TOPICLEN=450', - 'KICKLEN=450', - 'CHANNELLEN=30', - 'KEYLEN=23', - 'CHANTYPES=#', - 'PREFIX=(ov)@+', - 'CASEMAPPING=ascii', - 'CAPAB', - 'IRCD=dancer']) - + self._serverTestImpl( + "005", + msg, + "isupport", + args=args, + options=[ + "MODES=4", + "CHANLIMIT=#:20", + "NICKLEN=16", + "USERLEN=10", + "HOSTLEN=63", + "TOPICLEN=450", + "KICKLEN=450", + "CHANNELLEN=30", + "KEYLEN=23", + "CHANTYPES=#", + "PREFIX=(ov)@+", + "CASEMAPPING=ascii", + "CAPAB", + "IRCD=dancer", + ], + ) def test_ISUPPORT(self): """ The client parses ISUPPORT messages sent by the server and calls L{IRCClient.isupport}. """ self._sendISUPPORT() - def testBounce(self): msg = "Try server some.host, port 321" - self._serverTestImpl("010", msg, "bounce", - info=msg) - + self._serverTestImpl("010", msg, "bounce", info=msg) def testLuserChannels(self): args = "7116" msg = "channels formed" - self._serverTestImpl("254", msg, "luserChannels", args=args, - channels=int(args)) - + self._serverTestImpl("254", msg, "luserChannels", args=args, channels=int(args)) def testLuserOp(self): args = "34" msg = "flagged staff members" - self._serverTestImpl("252", msg, "luserOp", args=args, - ops=int(args)) - + self._serverTestImpl("252", msg, "luserOp", args=args, ops=int(args)) def testLuserMe(self): msg = "I have 1937 clients and 0 servers" - self._serverTestImpl("255", msg, "luserMe", - info=msg) - + self._serverTestImpl("255", msg, "luserMe", info=msg) def test_receivedMOTD(self): """ Lines received in I{RPL_MOTDSTART} and I{RPL_MOTD} are delivered to L{IRCClient.receivedMOTD} when I{RPL_ENDOFMOTD} is received. """ lines = [ ":host.name 375 nickname :- host.name Message of the Day -", ":host.name 372 nickname :- Welcome to host.name", - ":host.name 376 nickname :End of /MOTD command."] + ":host.name 376 nickname :End of /MOTD command.", + ] for L in lines: self.assertEqual(self.client.calls, []) - self.client.dataReceived(L + '\r\n') + self.client.dataReceived(L + "\r\n") self.assertEqual( self.client.calls, - [("receivedMOTD", {"motd": ["host.name Message of the Day -", "Welcome to host.name"]})]) + [ + ( + "receivedMOTD", + { + "motd": [ + "host.name Message of the Day -", + "Welcome to host.name", + ] + }, + ) + ], + ) # After the motd is delivered, the tracking variable should be # reset. self.assertIdentical(self.client.motd, None) - def test_withoutMOTDSTART(self): """ If L{IRCClient} receives I{RPL_MOTD} and I{RPL_ENDOFMOTD} without receiving I{RPL_MOTDSTART}, L{IRCClient.receivedMOTD} is still called with a list of MOTD lines. """ lines = [ ":host.name 372 nickname :- Welcome to host.name", - ":host.name 376 nickname :End of /MOTD command."] + ":host.name 376 nickname :End of /MOTD command.", + ] for L in lines: - self.client.dataReceived(L + '\r\n') - - self.assertEqual( - self.client.calls, - [("receivedMOTD", {"motd": ["Welcome to host.name"]})]) - + self.client.dataReceived(L + "\r\n") + + self.assertEqual( + self.client.calls, [("receivedMOTD", {"motd": ["Welcome to host.name"]})] + ) def _clientTestImpl(self, sender, group, type, msg, func, **kw): - ident = pop(kw, 'ident', 'ident') - host = pop(kw, 'host', 'host') - - wholeUser = sender + '!' + ident + '@' + host - message = (":" + - wholeUser + " " + - type + " " + - group + " :" + - msg + "\r\n") + ident = pop(kw, "ident", "ident") + host = pop(kw, "host", "host") + + wholeUser = sender + "!" + ident + "@" + host + message = ":" + wholeUser + " " + type + " " + group + " :" + msg + "\r\n" self.client.dataReceived(message) - self.assertEqual( - self.client.calls, - [(func, kw)]) + self.assertEqual(self.client.calls, [(func, kw)]) self.client.calls = [] - def testPrivmsg(self): msg = "Tooty toot toot." - self._clientTestImpl("sender", "#group", "PRIVMSG", msg, "privmsg", - ident="ident", host="host", - # Expected results below - user="sender!ident@host", - channel="#group", - message=msg) - - self._clientTestImpl("sender", "recipient", "PRIVMSG", msg, "privmsg", - ident="ident", host="host", - # Expected results below - user="sender!ident@host", - channel="recipient", - message=msg) - + self._clientTestImpl( + "sender", + "#group", + "PRIVMSG", + msg, + "privmsg", + ident="ident", + host="host", + # Expected results below + user="sender!ident@host", + channel="#group", + message=msg, + ) + + self._clientTestImpl( + "sender", + "recipient", + "PRIVMSG", + msg, + "privmsg", + ident="ident", + host="host", + # Expected results below + user="sender!ident@host", + channel="recipient", + message=msg, + ) def test_getChannelModeParams(self): """ L{IRCClient.getChannelModeParams} uses ISUPPORT information, either given by the server or defaults, to determine which channel modes require arguments when being added or removed. """ add, remove = map(sorted, self.client.getChannelModeParams()) - self.assertEqual(add, ['b', 'h', 'k', 'l', 'o', 'v']) - self.assertEqual(remove, ['b', 'h', 'o', 'v']) + self.assertEqual(add, ["b", "h", "k", "l", "o", "v"]) + self.assertEqual(remove, ["b", "h", "o", "v"]) def removeFeature(name): - name = '-' + name + name = "-" + name msg = "are available on this server" - self._serverTestImpl( - '005', msg, 'isupport', args=name, options=[name]) - self.assertIdentical( - self.client.supported.getFeature(name), None) + self._serverTestImpl("005", msg, "isupport", args=name, options=[name]) + self.assertIdentical(self.client.supported.getFeature(name), None) self.client.calls = [] # Remove CHANMODES feature, causing getFeature('CHANMODES') to return # None. - removeFeature('CHANMODES') + removeFeature("CHANMODES") add, remove = map(sorted, self.client.getChannelModeParams()) - self.assertEqual(add, ['h', 'o', 'v']) - self.assertEqual(remove, ['h', 'o', 'v']) + self.assertEqual(add, ["h", "o", "v"]) + self.assertEqual(remove, ["h", "o", "v"]) # Remove PREFIX feature, causing getFeature('PREFIX') to return None. - removeFeature('PREFIX') + removeFeature("PREFIX") add, remove = map(sorted, self.client.getChannelModeParams()) self.assertEqual(add, []) self.assertEqual(remove, []) # Restore ISUPPORT features. self._sendISUPPORT() - self.assertNotIdentical( - self.client.supported.getFeature('PREFIX'), None) - + self.assertNotIdentical(self.client.supported.getFeature("PREFIX"), None) def test_getUserModeParams(self): """ L{IRCClient.getUserModeParams} returns a list of user modes (modes that the user sets on themself, outside of channel modes) that require @@ -1464,193 +1341,177 @@ """ add, remove = map(sorted, self.client.getUserModeParams()) self.assertEqual(add, []) self.assertEqual(remove, []) - - def _sendModeChange(self, msg, args='', target=None): + def _sendModeChange(self, msg, args="", target=None): """ Build a MODE string and send it to the client. """ if target is None: - target = '#chan' - message = ":Wolf!~wolf@yok.utu.fi MODE %s %s %s\r\n" % ( - target, msg, args) + target = "#chan" + message = ":Wolf!~wolf@yok.utu.fi MODE %s %s %s\r\n" % (target, msg, args) self.client.dataReceived(message) - def _parseModeChange(self, results, target=None): """ Parse the results, do some test and return the data to check. """ if target is None: - target = '#chan' + target = "#chan" for n, result in enumerate(results): method, data = result - self.assertEqual(method, 'modeChanged') - self.assertEqual(data['user'], 'Wolf!~wolf@yok.utu.fi') - self.assertEqual(data['channel'], target) - results[n] = tuple([data[key] for key in ('set', 'modes', 'args')]) + self.assertEqual(method, "modeChanged") + self.assertEqual(data["user"], "Wolf!~wolf@yok.utu.fi") + self.assertEqual(data["channel"], target) + results[n] = tuple([data[key] for key in ("set", "modes", "args")]) return results - def _checkModeChange(self, expected, target=None): """ Compare the expected result with the one returned by the client. """ result = self._parseModeChange(self.client.calls, target) self.assertEqual(result, expected) self.client.calls = [] - def test_modeMissingDirection(self): """ Mode strings that do not begin with a directional character, C{'+'} or C{'-'}, have C{'+'} automatically prepended. """ - self._sendModeChange('s') - self._checkModeChange([(True, 's', (None,))]) - + self._sendModeChange("s") + self._checkModeChange([(True, "s", (None,))]) def test_noModeParameters(self): """ No parameters are passed to L{IRCClient.modeChanged} for modes that don't take any parameters. """ - self._sendModeChange('-s') - self._checkModeChange([(False, 's', (None,))]) - self._sendModeChange('+n') - self._checkModeChange([(True, 'n', (None,))]) - + self._sendModeChange("-s") + self._checkModeChange([(False, "s", (None,))]) + self._sendModeChange("+n") + self._checkModeChange([(True, "n", (None,))]) def test_oneModeParameter(self): """ Parameters are passed to L{IRCClient.modeChanged} for modes that take parameters. """ - self._sendModeChange('+o', 'a_user') - self._checkModeChange([(True, 'o', ('a_user',))]) - self._sendModeChange('-o', 'a_user') - self._checkModeChange([(False, 'o', ('a_user',))]) - + self._sendModeChange("+o", "a_user") + self._checkModeChange([(True, "o", ("a_user",))]) + self._sendModeChange("-o", "a_user") + self._checkModeChange([(False, "o", ("a_user",))]) def test_mixedModes(self): """ Mixing adding and removing modes that do and don't take parameters invokes L{IRCClient.modeChanged} with mode characters and parameters that match up. """ - self._sendModeChange('+osv', 'a_user another_user') - self._checkModeChange([(True, 'osv', ('a_user', None, 'another_user'))]) - self._sendModeChange('+v-os', 'a_user another_user') - self._checkModeChange([(True, 'v', ('a_user',)), - (False, 'os', ('another_user', None))]) - + self._sendModeChange("+osv", "a_user another_user") + self._checkModeChange([(True, "osv", ("a_user", None, "another_user"))]) + self._sendModeChange("+v-os", "a_user another_user") + self._checkModeChange( + [(True, "v", ("a_user",)), (False, "os", ("another_user", None))] + ) def test_tooManyModeParameters(self): """ Passing an argument to modes that take no parameters results in L{IRCClient.modeChanged} not being called and an error being logged. """ - self._sendModeChange('+s', 'wrong') + self._sendModeChange("+s", "wrong") self._checkModeChange([]) errors = self.flushLoggedErrors(irc.IRCBadModes) self.assertEqual(len(errors), 1) - self.assertSubstring( - 'Too many parameters', errors[0].getErrorMessage()) - + self.assertSubstring("Too many parameters", errors[0].getErrorMessage()) def test_tooFewModeParameters(self): """ Passing no arguments to modes that do take parameters results in L{IRCClient.modeChange} not being called and an error being logged. """ - self._sendModeChange('+o') + self._sendModeChange("+o") self._checkModeChange([]) errors = self.flushLoggedErrors(irc.IRCBadModes) self.assertEqual(len(errors), 1) - self.assertSubstring( - 'Not enough parameters', errors[0].getErrorMessage()) - + self.assertSubstring("Not enough parameters", errors[0].getErrorMessage()) def test_userMode(self): """ A C{MODE} message whose target is our user (the nickname of our user, to be precise), as opposed to a channel, will be parsed according to the modes specified by L{IRCClient.getUserModeParams}. """ target = self.client.nickname # Mode "o" on channels is supposed to take a parameter, but since this # is not a channel this will not cause an exception. - self._sendModeChange('+o', target=target) - self._checkModeChange([(True, 'o', (None,))], target=target) + self._sendModeChange("+o", target=target) + self._checkModeChange([(True, "o", (None,))], target=target) def getUserModeParams(): - return ['Z', ''] + return ["Z", ""] # Introduce our own user mode that takes an argument. - self.patch(self.client, 'getUserModeParams', getUserModeParams) - - self._sendModeChange('+Z', 'an_arg', target=target) - self._checkModeChange([(True, 'Z', ('an_arg',))], target=target) - + self.patch(self.client, "getUserModeParams", getUserModeParams) + + self._sendModeChange("+Z", "an_arg", target=target) + self._checkModeChange([(True, "Z", ("an_arg",))], target=target) def test_heartbeat(self): """ When the I{RPL_WELCOME} message is received a heartbeat is started that will send a I{PING} message to the IRC server every L{irc.IRCClient.heartbeatInterval} seconds. When the transport is closed the heartbeat looping call is stopped too. """ + def _createHeartbeat(): heartbeat = self._originalCreateHeartbeat() heartbeat.clock = self.clock return heartbeat self.clock = task.Clock() self._originalCreateHeartbeat = self.client._createHeartbeat - self.patch(self.client, '_createHeartbeat', _createHeartbeat) + self.patch(self.client, "_createHeartbeat", _createHeartbeat) self.assertIdentical(self.client._heartbeat, None) - self.client.irc_RPL_WELCOME('foo', []) + self.client.irc_RPL_WELCOME("foo", []) self.assertNotIdentical(self.client._heartbeat, None) - self.assertEqual(self.client.hostname, 'foo') + self.assertEqual(self.client.hostname, "foo") # Pump the clock enough to trigger one LoopingCall. - self.assertEqualBufferValue(self.transport.value(), '') + self.assertEqualBufferValue(self.transport.value(), "") self.clock.advance(self.client.heartbeatInterval) - self.assertEqualBufferValue(self.transport.value(), 'PING foo\r\n') + self.assertEqualBufferValue(self.transport.value(), "PING foo\r\n") # When the connection is lost the heartbeat is stopped. self.transport.loseConnection() self.client.connectionLost(None) - self.assertEqual( - len(self.clock.getDelayedCalls()), 0) + self.assertEqual(len(self.clock.getDelayedCalls()), 0) self.assertIdentical(self.client._heartbeat, None) - def test_heartbeatDisabled(self): """ If L{irc.IRCClient.heartbeatInterval} is set to L{None} then no heartbeat is created. """ self.assertIdentical(self.client._heartbeat, None) self.client.heartbeatInterval = None - self.client.irc_RPL_WELCOME('foo', []) + self.client.irc_RPL_WELCOME("foo", []) self.assertIdentical(self.client._heartbeat, None) - class BasicServerFunctionalityTests(IRCTestCase): def setUp(self): self.f = StringIOWithoutClosing() self.t = protocol.FileWrapper(self.f) self.p = irc.IRC() self.p.makeConnection(self.t) - def check(self, s): """ Make sure that the internal buffer equals a specified value. @param s: the value to compare against buffer @@ -1659,94 +1520,90 @@ bufferValue = self.f.getvalue() if isinstance(s, str): bufferValue = bufferValue.decode("utf-8") self.assertEqual(bufferValue, s) - def test_sendMessage(self): """ Passing a command and parameters to L{IRC.sendMessage} results in a query string that consists of the command and parameters, separated by a space, ending with '\r\n'. """ - self.p.sendMessage('CMD', 'param1', 'param2') - self.check('CMD param1 param2\r\n') - + self.p.sendMessage("CMD", "param1", "param2") + self.check("CMD param1 param2\r\n") def test_sendCommand(self): """ Passing a command and parameters to L{IRC.sendCommand} results in a query string that consists of the command and parameters, separated by a space, ending with '\r\n'. The format is described in more detail in U{RFC 1459 }. """ - self.p.sendCommand(u"CMD", (u"param1", u"param2")) + self.p.sendCommand("CMD", ("param1", "param2")) self.check("CMD param1 param2\r\n") - def test_sendUnicodeCommand(self): """ Passing unicode parameters to L{IRC.sendCommand} encodes the parameters in UTF-8. """ - self.p.sendCommand(u"CMD", (u"param\u00b9", u"param\u00b2")) + self.p.sendCommand("CMD", ("param\u00b9", "param\u00b2")) self.check(b"CMD param\xc2\xb9 param\xc2\xb2\r\n") - def test_sendMessageNoCommand(self): """ Passing L{None} as the command to L{IRC.sendMessage} raises a C{ValueError}. """ - error = self.assertRaises(ValueError, self.p.sendMessage, None, - 'param1', 'param2') + error = self.assertRaises( + ValueError, self.p.sendMessage, None, "param1", "param2" + ) self.assertEqual(str(error), "IRC message requires a command.") - def test_sendCommandNoCommand(self): """ Passing L{None} as the command to L{IRC.sendCommand} raises a C{ValueError}. """ - error = self.assertRaises(ValueError, self.p.sendCommand, None, - (u"param1", u"param2")) + error = self.assertRaises( + ValueError, self.p.sendCommand, None, ("param1", "param2") + ) self.assertEqual(error.args[0], "IRC message requires a command.") - def test_sendMessageInvalidCommand(self): """ Passing an invalid string command to L{IRC.sendMessage} raises a C{ValueError}. """ - error = self.assertRaises(ValueError, self.p.sendMessage, ' ', - 'param1', 'param2') - self.assertEqual(str(error), - "Somebody screwed up, 'cuz this doesn't look like a command to " - "me: ") - + error = self.assertRaises( + ValueError, self.p.sendMessage, " ", "param1", "param2" + ) + self.assertEqual( + str(error), + "Somebody screwed up, 'cuz this doesn't look like a command to " "me: ", + ) def test_sendCommandInvalidCommand(self): """ Passing an invalid string command to L{IRC.sendCommand} raises a C{ValueError}. """ - error = self.assertRaises(ValueError, self.p.sendCommand, u" ", - (u"param1", u"param2")) + error = self.assertRaises( + ValueError, self.p.sendCommand, " ", ("param1", "param2") + ) self.assertEqual(error.args[0], 'Invalid command: " "') - def test_sendCommandWithPrefix(self): """ Passing a command and parameters with a specified prefix to L{IRC.sendCommand} results in a proper query string including the specified line prefix. """ - self.p.sendCommand(u"CMD", (u"param1", u"param2"), u"irc.example.com") + self.p.sendCommand("CMD", ("param1", "param2"), "irc.example.com") self.check(b":irc.example.com CMD param1 param2\r\n") - def test_sendCommandWithTags(self): """ Passing a command and parameters with a specified prefix and tags to L{IRC.sendCommand} results in a proper query string including the @@ -1756,448 +1613,418 @@ The tags are a string of IRCv3 tags, preceded by '@'. The rest of the string is as described in test_sendMessage. For more on the message tag format, see U{the IRCv3 specification }. """ - sendTags = { - u"aaa": u"bbb", - u"ccc": None, - u"example.com/ddd": u"eee" - } + sendTags = {"aaa": "bbb", "ccc": None, "example.com/ddd": "eee"} expectedTags = (b"aaa=bbb", b"ccc", b"example.com/ddd=eee") - self.p.sendCommand(u"CMD", (u"param1", u"param2"), u"irc.example.com", - sendTags) - outMsg = self.f.getvalue() - outTagStr, outLine = outMsg.split(b' ', 1) - - # We pull off the leading '@' sign so that the split tags can be - # compared with what we expect. - outTags = outTagStr[1:].split(b';') - - self.assertEqual(outLine, b":irc.example.com CMD param1 param2\r\n") - self.assertEqual(sorted(expectedTags), sorted(outTags)) - - - def test_sendCommandValidateEmptyTags(self): - """ - Passing empty tag names to L{IRC.sendCommand} raises a C{ValueError}. - """ - sendTags = { - u"aaa": u"bbb", - u"ccc": None, - u"": u"" - } - error = self.assertRaises(ValueError, self.p.sendCommand, u"CMD", - (u"param1", u"param2"), u"irc.example.com", sendTags) - self.assertEqual(error.args[0], "A tag name is required.") - - - def test_sendCommandValidateNoneTags(self): - """ - Passing None as a tag name to L{IRC.sendCommand} raises a - C{ValueError}. - """ - sendTags = { - u"aaa": u"bbb", - u"ccc": None, - None: u"beep" - } - error = self.assertRaises(ValueError, self.p.sendCommand, u"CMD", - (u"param1", u"param2"), u"irc.example.com", sendTags) - self.assertEqual(error.args[0], "A tag name is required.") - - - def test_sendCommandValidateTagsWithSpaces(self): - """ - Passing a tag name containing spaces to L{IRC.sendCommand} raises a - C{ValueError}. - """ - sendTags = { - u"aaa bbb": u"ccc" - } - error = self.assertRaises(ValueError, self.p.sendCommand, u"CMD", - (u"param1", u"param2"), u"irc.example.com", sendTags) - self.assertEqual(error.args[0], "Tag contains invalid characters.") - - - def test_sendCommandValidateTagsWithInvalidChars(self): - """ - Passing a tag name containing invalid characters to L{IRC.sendCommand} - raises a C{ValueError}. - """ - sendTags = { - u"aaa_b^@": u"ccc" - } - error = self.assertRaises(ValueError, self.p.sendCommand, u"CMD", - (u"param1", u"param2"), u"irc.example.com", sendTags) - self.assertEqual(error.args[0], "Tag contains invalid characters.") - - - def test_sendCommandValidateTagValueEscaping(self): - """ - Tags with values containing invalid characters passed to - L{IRC.sendCommand} are escaped. - """ - sendTags = { - u"aaa": u"bbb", - u"ccc": u"test\r\n \\;;" - } - expectedTags = (b"aaa=bbb", b"ccc=test\\r\\n\\s\\\\\\:\\:") - self.p.sendCommand(u"CMD", (u"param1", u"param2"), u"irc.example.com", - sendTags) + self.p.sendCommand("CMD", ("param1", "param2"), "irc.example.com", sendTags) outMsg = self.f.getvalue() outTagStr, outLine = outMsg.split(b" ", 1) # We pull off the leading '@' sign so that the split tags can be # compared with what we expect. outTags = outTagStr[1:].split(b";") + self.assertEqual(outLine, b":irc.example.com CMD param1 param2\r\n") + self.assertEqual(sorted(expectedTags), sorted(outTags)) + + def test_sendCommandValidateEmptyTags(self): + """ + Passing empty tag names to L{IRC.sendCommand} raises a C{ValueError}. + """ + sendTags = {"aaa": "bbb", "ccc": None, "": ""} + error = self.assertRaises( + ValueError, + self.p.sendCommand, + "CMD", + ("param1", "param2"), + "irc.example.com", + sendTags, + ) + self.assertEqual(error.args[0], "A tag name is required.") + + def test_sendCommandValidateNoneTags(self): + """ + Passing None as a tag name to L{IRC.sendCommand} raises a + C{ValueError}. + """ + sendTags = {"aaa": "bbb", "ccc": None, None: "beep"} + error = self.assertRaises( + ValueError, + self.p.sendCommand, + "CMD", + ("param1", "param2"), + "irc.example.com", + sendTags, + ) + self.assertEqual(error.args[0], "A tag name is required.") + + def test_sendCommandValidateTagsWithSpaces(self): + """ + Passing a tag name containing spaces to L{IRC.sendCommand} raises a + C{ValueError}. + """ + sendTags = {"aaa bbb": "ccc"} + error = self.assertRaises( + ValueError, + self.p.sendCommand, + "CMD", + ("param1", "param2"), + "irc.example.com", + sendTags, + ) + self.assertEqual(error.args[0], "Tag contains invalid characters.") + + def test_sendCommandValidateTagsWithInvalidChars(self): + """ + Passing a tag name containing invalid characters to L{IRC.sendCommand} + raises a C{ValueError}. + """ + sendTags = {"aaa_b^@": "ccc"} + error = self.assertRaises( + ValueError, + self.p.sendCommand, + "CMD", + ("param1", "param2"), + "irc.example.com", + sendTags, + ) + self.assertEqual(error.args[0], "Tag contains invalid characters.") + + def test_sendCommandValidateTagValueEscaping(self): + """ + Tags with values containing invalid characters passed to + L{IRC.sendCommand} are escaped. + """ + sendTags = {"aaa": "bbb", "ccc": "test\r\n \\;;"} + expectedTags = (b"aaa=bbb", b"ccc=test\\r\\n\\s\\\\\\:\\:") + self.p.sendCommand("CMD", ("param1", "param2"), "irc.example.com", sendTags) + outMsg = self.f.getvalue() + outTagStr, outLine = outMsg.split(b" ", 1) + + # We pull off the leading '@' sign so that the split tags can be + # compared with what we expect. + outTags = outTagStr[1:].split(b";") + self.assertEqual(sorted(outTags), sorted(expectedTags)) - def testPrivmsg(self): self.p.privmsg("this-is-sender", "this-is-recip", "this is message") self.check(":this-is-sender PRIVMSG this-is-recip :this is message\r\n") - def testNotice(self): self.p.notice("this-is-sender", "this-is-recip", "this is notice") self.check(":this-is-sender NOTICE this-is-recip :this is notice\r\n") - def testAction(self): self.p.action("this-is-sender", "this-is-recip", "this is action") self.check(":this-is-sender ACTION this-is-recip :this is action\r\n") - def testJoin(self): self.p.join("this-person", "#this-channel") self.check(":this-person JOIN #this-channel\r\n") - def testPart(self): self.p.part("this-person", "#that-channel") self.check(":this-person PART #that-channel\r\n") - def testWhois(self): """ Verify that a whois by the client receives the right protocol actions from the server. """ - timestamp = int(time.time()-100) + timestamp = int(time.time() - 100) hostname = self.p.hostname - req = 'requesting-nick' - targ = 'target-nick' - self.p.whois(req, targ, 'target', 'host.com', - 'Target User', 'irc.host.com', 'A fake server', False, - 12, timestamp, ['#fakeusers', '#fakemisc']) - expected = '\r\n'.join([ -':%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User', -':%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server', -':%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time', -':%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc', -':%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.', -'']) % dict(hostname=hostname, timestamp=timestamp, req=req, targ=targ) + req = "requesting-nick" + targ = "target-nick" + self.p.whois( + req, + targ, + "target", + "host.com", + "Target User", + "irc.host.com", + "A fake server", + False, + 12, + timestamp, + ["#fakeusers", "#fakemisc"], + ) + expected = "\r\n".join( + [ + ":%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User", + ":%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server", + ":%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time", + ":%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc", + ":%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.", + "", + ] + ) % dict(hostname=hostname, timestamp=timestamp, req=req, targ=targ) self.check(expected) - class DummyClient(irc.IRCClient): """ A L{twisted.words.protocols.irc.IRCClient} that stores sent lines in a C{list} rather than transmitting them. """ + def __init__(self): self.lines = [] - def connectionMade(self): irc.IRCClient.connectionMade(self) self.lines = [] - def _truncateLine(self, line): """ Truncate an IRC line to the maximum allowed length. """ - return line[:irc.MAX_COMMAND_LENGTH - len(self.delimiter)] - + return line[: irc.MAX_COMMAND_LENGTH - len(self.delimiter)] def lineReceived(self, line): # Emulate IRC servers throwing away our important data. line = self._truncateLine(line) return irc.IRCClient.lineReceived(self, line) - def sendLine(self, m): self.lines.append(self._truncateLine(m)) - class ClientInviteTests(IRCTestCase): """ Tests for L{IRCClient.invite}. """ + def setUp(self): """ Create a L{DummyClient} to call C{invite} on in test methods. """ self.client = DummyClient() - def test_channelCorrection(self): """ If the channel name passed to L{IRCClient.invite} does not begin with a channel prefix character, one is prepended to it. """ - self.client.invite('foo', 'bar') - self.assertEqual(self.client.lines, ['INVITE foo #bar']) - + self.client.invite("foo", "bar") + self.assertEqual(self.client.lines, ["INVITE foo #bar"]) def test_invite(self): """ L{IRCClient.invite} sends an I{INVITE} message with the specified username and a channel. """ - self.client.invite('foo', '#bar') - self.assertEqual(self.client.lines, ['INVITE foo #bar']) - + self.client.invite("foo", "#bar") + self.assertEqual(self.client.lines, ["INVITE foo #bar"]) class ClientMsgTests(IRCTestCase): """ Tests for messages sent with L{twisted.words.protocols.irc.IRCClient}. """ + def setUp(self): self.client = DummyClient() self.client.connectionMade() - def test_singleLine(self): """ A message containing no newlines is sent in a single command. """ - self.client.msg('foo', 'bar') - self.assertEqual(self.client.lines, ['PRIVMSG foo :bar']) - + self.client.msg("foo", "bar") + self.assertEqual(self.client.lines, ["PRIVMSG foo :bar"]) def test_invalidMaxLength(self): """ Specifying a C{length} value to L{IRCClient.msg} that is too short to contain the protocol command to send a message raises C{ValueError}. """ - self.assertRaises(ValueError, self.client.msg, 'foo', 'bar', 0) - self.assertRaises(ValueError, self.client.msg, 'foo', 'bar', 3) - + self.assertRaises(ValueError, self.client.msg, "foo", "bar", 0) + self.assertRaises(ValueError, self.client.msg, "foo", "bar", 3) def test_multipleLine(self): """ Messages longer than the C{length} parameter to L{IRCClient.msg} will be split and sent in multiple commands. """ - maxLen = len('PRIVMSG foo :') + 3 + 2 # 2 for line endings - self.client.msg('foo', 'barbazbo', maxLen) + maxLen = len("PRIVMSG foo :") + 3 + 2 # 2 for line endings + self.client.msg("foo", "barbazbo", maxLen) self.assertEqual( self.client.lines, - ['PRIVMSG foo :bar', - 'PRIVMSG foo :baz', - 'PRIVMSG foo :bo']) - + ["PRIVMSG foo :bar", "PRIVMSG foo :baz", "PRIVMSG foo :bo"], + ) def test_sufficientWidth(self): """ Messages exactly equal in length to the C{length} parameter to L{IRCClient.msg} are sent in a single command. """ - msg = 'barbazbo' - maxLen = len('PRIVMSG foo :%s' % (msg,)) + 2 - self.client.msg('foo', msg, maxLen) - self.assertEqual(self.client.lines, ['PRIVMSG foo :%s' % (msg,)]) + msg = "barbazbo" + maxLen = len("PRIVMSG foo :%s" % (msg,)) + 2 + self.client.msg("foo", msg, maxLen) + self.assertEqual(self.client.lines, ["PRIVMSG foo :%s" % (msg,)]) self.client.lines = [] - self.client.msg('foo', msg, maxLen-1) + self.client.msg("foo", msg, maxLen - 1) self.assertEqual(2, len(self.client.lines)) self.client.lines = [] - self.client.msg('foo', msg, maxLen+1) + self.client.msg("foo", msg, maxLen + 1) self.assertEqual(1, len(self.client.lines)) - def test_newlinesAtStart(self): """ An LF at the beginning of the message is ignored. """ self.client.lines = [] - self.client.msg('foo', '\nbar') - self.assertEqual(self.client.lines, ['PRIVMSG foo :bar']) - + self.client.msg("foo", "\nbar") + self.assertEqual(self.client.lines, ["PRIVMSG foo :bar"]) def test_newlinesAtEnd(self): """ An LF at the end of the message is ignored. """ self.client.lines = [] - self.client.msg('foo', 'bar\n') - self.assertEqual(self.client.lines, ['PRIVMSG foo :bar']) - + self.client.msg("foo", "bar\n") + self.assertEqual(self.client.lines, ["PRIVMSG foo :bar"]) def test_newlinesWithinMessage(self): """ An LF within a message causes a new line. """ self.client.lines = [] - self.client.msg('foo', 'bar\nbaz') - self.assertEqual( - self.client.lines, - ['PRIVMSG foo :bar', - 'PRIVMSG foo :baz']) - + self.client.msg("foo", "bar\nbaz") + self.assertEqual(self.client.lines, ["PRIVMSG foo :bar", "PRIVMSG foo :baz"]) def test_consecutiveNewlines(self): """ Consecutive LFs do not cause a blank line. """ self.client.lines = [] - self.client.msg('foo', 'bar\n\nbaz') - self.assertEqual( - self.client.lines, - ['PRIVMSG foo :bar', - 'PRIVMSG foo :baz']) - - - def assertLongMessageSplitting(self, message, expectedNumCommands, - length=None): + self.client.msg("foo", "bar\n\nbaz") + self.assertEqual(self.client.lines, ["PRIVMSG foo :bar", "PRIVMSG foo :baz"]) + + def assertLongMessageSplitting(self, message, expectedNumCommands, length=None): """ Assert that messages sent by L{IRCClient.msg} are split into an expected number of commands and the original message is transmitted in its entirety over those commands. """ - responsePrefix = ':%s!%s@%s ' % ( + responsePrefix = ":%s!%s@%s " % ( self.client.nickname, self.client.realname, - self.client.hostname) - - self.client.msg('foo', message, length=length) + self.client.hostname, + ) + + self.client.msg("foo", message, length=length) privmsg = [] - self.patch(self.client, 'privmsg', lambda *a: privmsg.append(a)) + self.patch(self.client, "privmsg", lambda *a: privmsg.append(a)) # Deliver these to IRCClient via the normal mechanisms. for line in self.client.lines: self.client.lineReceived(responsePrefix + line) self.assertEqual(len(privmsg), expectedNumCommands) - receivedMessage = ''.join( - message for user, target, message in privmsg) + receivedMessage = "".join(message for user, target, message in privmsg) # Did the long message we sent arrive as intended? self.assertEqual(message, receivedMessage) - def test_splitLongMessagesWithDefault(self): """ If a maximum message length is not provided to L{IRCClient.msg} a best-guess effort is made to determine a safe maximum, messages longer than this are split into multiple commands with the intent of delivering long messages without losing data due to message truncation when the server relays them. """ - message = 'o' * (irc.MAX_COMMAND_LENGTH - 2) + message = "o" * (irc.MAX_COMMAND_LENGTH - 2) self.assertLongMessageSplitting(message, 2) - def test_splitLongMessagesWithOverride(self): """ The maximum message length can be specified to L{IRCClient.msg}, messages longer than this are split into multiple commands with the intent of delivering long messages without losing data due to message truncation when the server relays them. """ - message = 'o' * (irc.MAX_COMMAND_LENGTH - 2) - self.assertLongMessageSplitting( - message, 3, length=irc.MAX_COMMAND_LENGTH // 2) - + message = "o" * (irc.MAX_COMMAND_LENGTH - 2) + self.assertLongMessageSplitting(message, 3, length=irc.MAX_COMMAND_LENGTH // 2) def test_newlinesBeforeLineBreaking(self): """ IRCClient breaks on newlines before it breaks long lines. """ # Because MAX_COMMAND_LENGTH includes framing characters, this long # line is slightly longer than half the permissible message size. - longline = 'o' * (irc.MAX_COMMAND_LENGTH // 2) - - self.client.msg('foo', longline + '\n' + longline) - self.assertEqual( - self.client.lines, - ['PRIVMSG foo :' + longline, - 'PRIVMSG foo :' + longline]) - + longline = "o" * (irc.MAX_COMMAND_LENGTH // 2) + + self.client.msg("foo", longline + "\n" + longline) + self.assertEqual( + self.client.lines, ["PRIVMSG foo :" + longline, "PRIVMSG foo :" + longline] + ) def test_lineBreakOnWordBoundaries(self): """ IRCClient prefers to break long lines at word boundaries. """ # Because MAX_COMMAND_LENGTH includes framing characters, this long # line is slightly longer than half the permissible message size. - longline = 'o' * (irc.MAX_COMMAND_LENGTH // 2) - - self.client.msg('foo', longline + ' ' + longline) - self.assertEqual( - self.client.lines, - ['PRIVMSG foo :' + longline, - 'PRIVMSG foo :' + longline]) - + longline = "o" * (irc.MAX_COMMAND_LENGTH // 2) + + self.client.msg("foo", longline + " " + longline) + self.assertEqual( + self.client.lines, ["PRIVMSG foo :" + longline, "PRIVMSG foo :" + longline] + ) def test_splitSanity(self): """ L{twisted.words.protocols.irc.split} raises C{ValueError} if given a length less than or equal to C{0} and returns C{[]} when splitting C{''}. """ # Whiteboxing - self.assertRaises(ValueError, irc.split, 'foo', -1) - self.assertRaises(ValueError, irc.split, 'foo', 0) - self.assertEqual([], irc.split('', 1)) - self.assertEqual([], irc.split('')) - + self.assertRaises(ValueError, irc.split, "foo", -1) + self.assertRaises(ValueError, irc.split, "foo", 0) + self.assertEqual([], irc.split("", 1)) + self.assertEqual([], irc.split("")) def test_splitDelimiters(self): """ L{twisted.words.protocols.irc.split} skips any delimiter (space or newline) that it finds at the very beginning of the string segment it is operating on. Nothing should be added to the output list because of it. """ r = irc.split("xx yyz", 2) - self.assertEqual(['xx', 'yy', 'z'], r) + self.assertEqual(["xx", "yy", "z"], r) r = irc.split("xx\nyyz", 2) - self.assertEqual(['xx', 'yy', 'z'], r) - + self.assertEqual(["xx", "yy", "z"], r) def test_splitValidatesLength(self): """ L{twisted.words.protocols.irc.split} raises C{ValueError} if given a length less than or equal to C{0}. """ self.assertRaises(ValueError, irc.split, "foo", 0) self.assertRaises(ValueError, irc.split, "foo", -1) - def test_say(self): """ L{IRCClient.say} prepends the channel prefix C{"#"} if necessary and then sends the message to the server for delivery to that channel. """ self.client.say("thechannel", "the message") - self.assertEqual( - self.client.lines, ["PRIVMSG #thechannel :the message"]) - + self.assertEqual(self.client.lines, ["PRIVMSG #thechannel :the message"]) class ClientTests(IRCTestCase): """ Tests for the protocol-level behavior of IRCClient methods intended to be called by application code. """ + def setUp(self): """ Create and connect a new L{IRCClient} to a new L{StringTransport}. """ self.transport = StringTransport() @@ -2210,642 +2037,684 @@ self.assertEqualBufferValue(self.transport.value(), "") self.addCleanup(self.transport.loseConnection) self.addCleanup(self.protocol.connectionLost, None) - def getLastLine(self, transport): """ Return the last IRC message in the transport buffer. """ line = transport.value() if bytes != str and isinstance(line, bytes): line = line.decode("utf-8") - return line.split('\r\n')[-2] - + return line.split("\r\n")[-2] def test_away(self): """ L{IRCClient.away} sends an AWAY command with the specified message. """ message = "Sorry, I'm not here." self.protocol.away(message) expected = [ - 'AWAY :%s' % (message,), - '', + "AWAY :%s" % (message,), + "", ] - self.assertEqualBufferValue(self.transport.value().split(b'\r\n'), expected) - + self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_back(self): """ L{IRCClient.back} sends an AWAY command with an empty message. """ self.protocol.back() expected = [ - 'AWAY :', - '', + "AWAY :", + "", ] - self.assertEqualBufferValue(self.transport.value().split(b'\r\n'), expected) - + self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_whois(self): """ L{IRCClient.whois} sends a WHOIS message. """ - self.protocol.whois('alice') + self.protocol.whois("alice") self.assertEqualBufferValue( - self.transport.value().split(b'\r\n'), - ['WHOIS alice', '']) - + self.transport.value().split(b"\r\n"), ["WHOIS alice", ""] + ) def test_whoisWithServer(self): """ L{IRCClient.whois} sends a WHOIS message with a server name if a value is passed for the C{server} parameter. """ - self.protocol.whois('alice', 'example.org') + self.protocol.whois("alice", "example.org") self.assertEqualBufferValue( - self.transport.value().split(b'\r\n'), - ['WHOIS example.org alice', '']) - + self.transport.value().split(b"\r\n"), ["WHOIS example.org alice", ""] + ) def test_register(self): """ L{IRCClient.register} sends NICK and USER commands with the username, name, hostname, server name, and real name specified. """ - username = 'testuser' - hostname = 'testhost' - servername = 'testserver' - self.protocol.realname = 'testname' + username = "testuser" + hostname = "testhost" + servername = "testserver" + self.protocol.realname = "testname" self.protocol.password = None self.protocol.register(username, hostname, servername) expected = [ - 'NICK %s' % (username,), - 'USER %s %s %s :%s' % ( - username, hostname, servername, self.protocol.realname), - ''] - self.assertEqualBufferValue(self.transport.value().split(b'\r\n'), expected) - + "NICK %s" % (username,), + "USER %s %s %s :%s" + % (username, hostname, servername, self.protocol.realname), + "", + ] + self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_registerWithPassword(self): """ If the C{password} attribute of L{IRCClient} is not L{None}, the C{register} method also sends a PASS command with it as the argument. """ - username = 'testuser' - hostname = 'testhost' - servername = 'testserver' - self.protocol.realname = 'testname' - self.protocol.password = 'testpass' + username = "testuser" + hostname = "testhost" + servername = "testserver" + self.protocol.realname = "testname" + self.protocol.password = "testpass" self.protocol.register(username, hostname, servername) expected = [ - 'PASS %s' % (self.protocol.password,), - 'NICK %s' % (username,), - 'USER %s %s %s :%s' % ( - username, hostname, servername, self.protocol.realname), - ''] - self.assertEqualBufferValue(self.transport.value().split(b'\r\n'), expected) - + "PASS %s" % (self.protocol.password,), + "NICK %s" % (username,), + "USER %s %s %s :%s" + % (username, hostname, servername, self.protocol.realname), + "", + ] + self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_registerWithTakenNick(self): """ Verify that the client repeats the L{IRCClient.setNick} method with a new value when presented with an C{ERR_NICKNAMEINUSE} while trying to register. """ - username = 'testuser' - hostname = 'testhost' - servername = 'testserver' - self.protocol.realname = 'testname' - self.protocol.password = 'testpass' + username = "testuser" + hostname = "testhost" + servername = "testserver" + self.protocol.realname = "testname" + self.protocol.password = "testpass" self.protocol.register(username, hostname, servername) - self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param']) + self.protocol.irc_ERR_NICKNAMEINUSE("prefix", ["param"]) lastLine = self.getLastLine(self.transport) - self.assertNotEqual(lastLine, 'NICK %s' % (username,)) + self.assertNotEqual(lastLine, "NICK %s" % (username,)) # Keep chaining underscores for each collision - self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param']) + self.protocol.irc_ERR_NICKNAMEINUSE("prefix", ["param"]) lastLine = self.getLastLine(self.transport) - self.assertEqual(lastLine, 'NICK %s' % (username + '__',)) - + self.assertEqual(lastLine, "NICK %s" % (username + "__",)) def test_overrideAlterCollidedNick(self): """ L{IRCClient.alterCollidedNick} determines how a nickname is altered upon collision while a user is trying to change to that nickname. """ - nick = 'foo' - self.protocol.alterCollidedNick = lambda nick: nick + '***' + nick = "foo" + self.protocol.alterCollidedNick = lambda nick: nick + "***" self.protocol.register(nick) - self.protocol.irc_ERR_NICKNAMEINUSE('prefix', ['param']) + self.protocol.irc_ERR_NICKNAMEINUSE("prefix", ["param"]) lastLine = self.getLastLine(self.transport) - self.assertEqual( - lastLine, 'NICK %s' % (nick + '***',)) - + self.assertEqual(lastLine, "NICK %s" % (nick + "***",)) def test_nickChange(self): """ When a NICK command is sent after signon, C{IRCClient.nickname} is set to the new nickname I{after} the server sends an acknowledgement. """ - oldnick = 'foo' - newnick = 'bar' + oldnick = "foo" + newnick = "bar" self.protocol.register(oldnick) - self.protocol.irc_RPL_WELCOME('prefix', ['param']) + self.protocol.irc_RPL_WELCOME("prefix", ["param"]) self.protocol.setNick(newnick) self.assertEqual(self.protocol.nickname, oldnick) - self.protocol.irc_NICK('%s!quux@qux' % (oldnick,), [newnick]) + self.protocol.irc_NICK("%s!quux@qux" % (oldnick,), [newnick]) self.assertEqual(self.protocol.nickname, newnick) - def test_erroneousNick(self): """ Trying to register an illegal nickname results in the default legal nickname being set, and trying to change a nickname to an illegal nickname results in the old nickname being kept. """ # Registration case: change illegal nickname to erroneousNickFallback - badnick = 'foo' + badnick = "foo" self.assertEqual(self.protocol._registered, False) self.protocol.register(badnick) - self.protocol.irc_ERR_ERRONEUSNICKNAME('prefix', ['param']) + self.protocol.irc_ERR_ERRONEUSNICKNAME("prefix", ["param"]) lastLine = self.getLastLine(self.transport) - self.assertEqual( - lastLine, 'NICK %s' % (self.protocol.erroneousNickFallback,)) - self.protocol.irc_RPL_WELCOME('prefix', ['param']) + self.assertEqual(lastLine, "NICK %s" % (self.protocol.erroneousNickFallback,)) + self.protocol.irc_RPL_WELCOME("prefix", ["param"]) self.assertEqual(self.protocol._registered, True) self.protocol.setNick(self.protocol.erroneousNickFallback) - self.assertEqual( - self.protocol.nickname, self.protocol.erroneousNickFallback) + self.assertEqual(self.protocol.nickname, self.protocol.erroneousNickFallback) # Illegal nick change attempt after registration. Fall back to the old # nickname instead of erroneousNickFallback. oldnick = self.protocol.nickname self.protocol.setNick(badnick) - self.protocol.irc_ERR_ERRONEUSNICKNAME('prefix', ['param']) + self.protocol.irc_ERR_ERRONEUSNICKNAME("prefix", ["param"]) lastLine = self.getLastLine(self.transport) - self.assertEqual( - lastLine, 'NICK %s' % (badnick,)) + self.assertEqual(lastLine, "NICK %s" % (badnick,)) self.assertEqual(self.protocol.nickname, oldnick) - def test_describe(self): """ L{IRCClient.desrcibe} sends a CTCP ACTION message to the target specified. """ - target = 'foo' - channel = '#bar' - action = 'waves' + target = "foo" + channel = "#bar" + action = "waves" self.protocol.describe(target, action) self.protocol.describe(channel, action) expected = [ - 'PRIVMSG %s :\01ACTION %s\01' % (target, action), - 'PRIVMSG %s :\01ACTION %s\01' % (channel, action), - ''] - self.assertEqualBufferValue(self.transport.value().split(b'\r\n'), expected) - + "PRIVMSG %s :\01ACTION %s\01" % (target, action), + "PRIVMSG %s :\01ACTION %s\01" % (channel, action), + "", + ] + self.assertEqualBufferValue(self.transport.value().split(b"\r\n"), expected) def test_noticedDoesntPrivmsg(self): """ The default implementation of L{IRCClient.noticed} doesn't invoke C{privmsg()} """ + def privmsg(user, channel, message): self.fail("privmsg() should not have been called") + self.protocol.privmsg = privmsg - self.protocol.irc_NOTICE( - 'spam', ['#greasyspooncafe', "I don't want any spam!"]) - + self.protocol.irc_NOTICE("spam", ["#greasyspooncafe", "I don't want any spam!"]) def test_ping(self): """ L{IRCClient.ping} """ # Ping a user with no message self.protocol.ping("otheruser") self.assertTrue( - self.transport.value().startswith(b'PRIVMSG otheruser :\x01PING')) + self.transport.value().startswith(b"PRIVMSG otheruser :\x01PING") + ) self.transport.clear() # Ping a user with a message self.protocol.ping("otheruser", "are you there") - self.assertEqual(self.transport.value(), - b'PRIVMSG otheruser :\x01PING are you there\x01\r\n') + self.assertEqual( + self.transport.value(), b"PRIVMSG otheruser :\x01PING are you there\x01\r\n" + ) self.transport.clear() # Create a lot of pings, more than MAX_PINGRING self.protocol._pings = {} for pingNum in range(self.protocol._MAX_PINGRING + 3): - self.protocol._pings[("otheruser"), (str(pingNum))] = ( - time.time() + pingNum) + self.protocol._pings[("otheruser"), (str(pingNum))] = time.time() + pingNum self.assertEqual(len(self.protocol._pings), self.protocol._MAX_PINGRING + 3) # Ping a user self.protocol.ping("otheruser", "I sent a lot of pings") # The excess pings should have been purged self.assertEqual(len(self.protocol._pings), self.protocol._MAX_PINGRING) - self.assertEqual(self.transport.value(), - b'PRIVMSG otheruser :\x01PING I sent a lot of pings\x01\r\n') - + self.assertEqual( + self.transport.value(), + b"PRIVMSG otheruser :\x01PING I sent a lot of pings\x01\r\n", + ) class CollectorClient(irc.IRCClient): """ A client that saves in a list the names of the methods that got called. """ + def __init__(self, methodsList): """ @param methodsList: list of methods' names that should be replaced. @type methodsList: C{list} """ self.methods = [] - self.nickname = 'Wolf' + self.nickname = "Wolf" for method in methodsList: + def fake_method(method=method): """ Collects C{method}s. """ + def inner(*args): self.methods.append((method, args)) + return inner + setattr(self, method, fake_method()) - class DccTests(IRCTestCase): """ Tests for C{dcc_*} methods. """ + def setUp(self): - methods = ['dccDoSend', 'dccDoAcceptResume', 'dccDoResume', - 'dccDoChat'] - self.user = 'Wolf!~wolf@yok.utu.fi' - self.channel = '#twisted' + methods = ["dccDoSend", "dccDoAcceptResume", "dccDoResume", "dccDoChat"] + self.user = "Wolf!~wolf@yok.utu.fi" + self.channel = "#twisted" self.client = CollectorClient(methods) - def test_dccSend(self): """ L{irc.IRCClient.dcc_SEND} invokes L{irc.IRCClient.dccDoSend}. """ - self.client.dcc_SEND(self.user, self.channel, 'foo.txt 127.0.0.1 1025') - self.assertEqual(self.client.methods, - [('dccDoSend', (self.user, '127.0.0.1', 1025, 'foo.txt', -1, - ['foo.txt', '127.0.0.1', '1025']))]) - + self.client.dcc_SEND(self.user, self.channel, "foo.txt 127.0.0.1 1025") + self.assertEqual( + self.client.methods, + [ + ( + "dccDoSend", + ( + self.user, + "127.0.0.1", + 1025, + "foo.txt", + -1, + ["foo.txt", "127.0.0.1", "1025"], + ), + ) + ], + ) def test_dccSendNotImplemented(self): """ L{irc.IRCClient.dccDoSend} is raises C{NotImplementedError} """ client = irc.IRCClient() - self.assertRaises(NotImplementedError, - client.dccSend, 'username', None) - + self.assertRaises(NotImplementedError, client.dccSend, "username", None) def test_dccSendMalformedRequest(self): """ L{irc.IRCClient.dcc_SEND} raises L{irc.IRCBadMessage} when it is passed a malformed query string. """ - result = self.assertRaises(irc.IRCBadMessage, self.client.dcc_SEND, - self.user, self.channel, 'foo') + result = self.assertRaises( + irc.IRCBadMessage, self.client.dcc_SEND, self.user, self.channel, "foo" + ) self.assertEqual(str(result), "malformed DCC SEND request: ['foo']") - def test_dccSendIndecipherableAddress(self): """ L{irc.IRCClient.dcc_SEND} raises L{irc.IRCBadMessage} when it is passed a query string that doesn't contain a valid address. """ - result = self.assertRaises(irc.IRCBadMessage, self.client.dcc_SEND, - self.user, self.channel, 'foo.txt #23 sd@d') + result = self.assertRaises( + irc.IRCBadMessage, + self.client.dcc_SEND, + self.user, + self.channel, + "foo.txt #23 sd@d", + ) self.assertEqual(str(result), "Indecipherable address '#23'") - def test_dccSendIndecipherablePort(self): """ L{irc.IRCClient.dcc_SEND} raises L{irc.IRCBadMessage} when it is passed a query string that doesn't contain a valid port number. """ - result = self.assertRaises(irc.IRCBadMessage, self.client.dcc_SEND, - self.user, self.channel, 'foo.txt 127.0.0.1 sd@d') + result = self.assertRaises( + irc.IRCBadMessage, + self.client.dcc_SEND, + self.user, + self.channel, + "foo.txt 127.0.0.1 sd@d", + ) self.assertEqual(str(result), "Indecipherable port 'sd@d'") - def test_dccAccept(self): """ L{irc.IRCClient.dcc_ACCEPT} invokes L{irc.IRCClient.dccDoAcceptResume}. """ - self.client.dcc_ACCEPT(self.user, self.channel, 'foo.txt 1025 2') - self.assertEqual(self.client.methods, - [('dccDoAcceptResume', (self.user, 'foo.txt', 1025, 2))]) - + self.client.dcc_ACCEPT(self.user, self.channel, "foo.txt 1025 2") + self.assertEqual( + self.client.methods, + [("dccDoAcceptResume", (self.user, "foo.txt", 1025, 2))], + ) def test_dccAcceptMalformedRequest(self): """ L{irc.IRCClient.dcc_ACCEPT} raises L{irc.IRCBadMessage} when it is passed a malformed query string. """ - result = self.assertRaises(irc.IRCBadMessage, self.client.dcc_ACCEPT, - self.user, self.channel, 'foo') - self.assertEqual(str(result), - "malformed DCC SEND ACCEPT request: ['foo']") - + result = self.assertRaises( + irc.IRCBadMessage, self.client.dcc_ACCEPT, self.user, self.channel, "foo" + ) + self.assertEqual(str(result), "malformed DCC SEND ACCEPT request: ['foo']") def test_dccResume(self): """ L{irc.IRCClient.dcc_RESUME} invokes L{irc.IRCClient.dccDoResume}. """ - self.client.dcc_RESUME(self.user, self.channel, 'foo.txt 1025 2') - self.assertEqual(self.client.methods, - [('dccDoResume', (self.user, 'foo.txt', 1025, 2))]) - + self.client.dcc_RESUME(self.user, self.channel, "foo.txt 1025 2") + self.assertEqual( + self.client.methods, [("dccDoResume", (self.user, "foo.txt", 1025, 2))] + ) def test_dccResumeMalformedRequest(self): """ L{irc.IRCClient.dcc_RESUME} raises L{irc.IRCBadMessage} when it is passed a malformed query string. """ - result = self.assertRaises(irc.IRCBadMessage, self.client.dcc_RESUME, - self.user, self.channel, 'foo') - self.assertEqual(str(result), - "malformed DCC SEND RESUME request: ['foo']") - + result = self.assertRaises( + irc.IRCBadMessage, self.client.dcc_RESUME, self.user, self.channel, "foo" + ) + self.assertEqual(str(result), "malformed DCC SEND RESUME request: ['foo']") def test_dccChat(self): """ L{irc.IRCClient.dcc_CHAT} invokes L{irc.IRCClient.dccDoChat}. """ - self.client.dcc_CHAT(self.user, self.channel, 'foo.txt 127.0.0.1 1025') - self.assertEqual(self.client.methods, - [('dccDoChat', (self.user, self.channel, '127.0.0.1', 1025, - ['foo.txt', '127.0.0.1', '1025']))]) - + self.client.dcc_CHAT(self.user, self.channel, "foo.txt 127.0.0.1 1025") + self.assertEqual( + self.client.methods, + [ + ( + "dccDoChat", + ( + self.user, + self.channel, + "127.0.0.1", + 1025, + ["foo.txt", "127.0.0.1", "1025"], + ), + ) + ], + ) def test_dccChatMalformedRequest(self): """ L{irc.IRCClient.dcc_CHAT} raises L{irc.IRCBadMessage} when it is passed a malformed query string. """ - result = self.assertRaises(irc.IRCBadMessage, self.client.dcc_CHAT, - self.user, self.channel, 'foo') - self.assertEqual(str(result), - "malformed DCC CHAT request: ['foo']") - + result = self.assertRaises( + irc.IRCBadMessage, self.client.dcc_CHAT, self.user, self.channel, "foo" + ) + self.assertEqual(str(result), "malformed DCC CHAT request: ['foo']") def test_dccChatIndecipherablePort(self): """ L{irc.IRCClient.dcc_CHAT} raises L{irc.IRCBadMessage} when it is passed a query string that doesn't contain a valid port number. """ - result = self.assertRaises(irc.IRCBadMessage, self.client.dcc_CHAT, - self.user, self.channel, 'foo.txt 127.0.0.1 sd@d') + result = self.assertRaises( + irc.IRCBadMessage, + self.client.dcc_CHAT, + self.user, + self.channel, + "foo.txt 127.0.0.1 sd@d", + ) self.assertEqual(str(result), "Indecipherable port 'sd@d'") - class ServerToClientTests(IRCTestCase): """ Tests for the C{irc_*} methods sent from the server to the client. """ + def setUp(self): - self.user = 'Wolf!~wolf@yok.utu.fi' - self.channel = '#twisted' - methods = ['joined', 'userJoined', 'left', 'userLeft', 'userQuit', - 'noticed', 'kickedFrom', 'userKicked', 'topicUpdated'] + self.user = "Wolf!~wolf@yok.utu.fi" + self.channel = "#twisted" + methods = [ + "joined", + "userJoined", + "left", + "userLeft", + "userQuit", + "noticed", + "kickedFrom", + "userKicked", + "topicUpdated", + ] self.client = CollectorClient(methods) - def test_irc_JOIN(self): """ L{IRCClient.joined} is called when I join a channel; L{IRCClient.userJoined} is called when someone else joins. """ self.client.irc_JOIN(self.user, [self.channel]) - self.client.irc_JOIN('Svadilfari!~svadi@yok.utu.fi', ['#python']) - self.assertEqual(self.client.methods, - [('joined', (self.channel,)), - ('userJoined', ('Svadilfari', '#python'))]) - + self.client.irc_JOIN("Svadilfari!~svadi@yok.utu.fi", ["#python"]) + self.assertEqual( + self.client.methods, + [("joined", (self.channel,)), ("userJoined", ("Svadilfari", "#python"))], + ) def test_irc_PART(self): """ L{IRCClient.left} is called when I part the channel; L{IRCClient.userLeft} is called when someone else parts. """ self.client.irc_PART(self.user, [self.channel]) - self.client.irc_PART('Svadilfari!~svadi@yok.utu.fi', ['#python']) - self.assertEqual(self.client.methods, - [('left', (self.channel,)), - ('userLeft', ('Svadilfari', '#python'))]) - + self.client.irc_PART("Svadilfari!~svadi@yok.utu.fi", ["#python"]) + self.assertEqual( + self.client.methods, + [("left", (self.channel,)), ("userLeft", ("Svadilfari", "#python"))], + ) def test_irc_QUIT(self): """ L{IRCClient.userQuit} is called whenever someone quits the channel (myself included). """ - self.client.irc_QUIT('Svadilfari!~svadi@yok.utu.fi', ['Adios.']) - self.client.irc_QUIT(self.user, ['Farewell.']) - self.assertEqual(self.client.methods, - [('userQuit', ('Svadilfari', 'Adios.')), - ('userQuit', ('Wolf', 'Farewell.'))]) - + self.client.irc_QUIT("Svadilfari!~svadi@yok.utu.fi", ["Adios."]) + self.client.irc_QUIT(self.user, ["Farewell."]) + self.assertEqual( + self.client.methods, + [ + ("userQuit", ("Svadilfari", "Adios.")), + ("userQuit", ("Wolf", "Farewell.")), + ], + ) def test_irc_NOTICE(self): """ L{IRCClient.noticed} is called when a notice is received. """ - msg = ('%(X)cextended%(X)cdata1%(X)cextended%(X)cdata2%(X)c%(EOL)s' % - {'X': irc.X_DELIM, 'EOL': irc.CR + irc.LF}) + msg = "%(X)cextended%(X)cdata1%(X)cextended%(X)cdata2%(X)c%(EOL)s" % { + "X": irc.X_DELIM, + "EOL": irc.CR + irc.LF, + } self.client.irc_NOTICE(self.user, [self.channel, msg]) - self.assertEqual(self.client.methods, - [('noticed', (self.user, '#twisted', 'data1 data2'))]) - + self.assertEqual( + self.client.methods, [("noticed", (self.user, "#twisted", "data1 data2"))] + ) def test_irc_KICK(self): """ L{IRCClient.kickedFrom} is called when I get kicked from the channel; L{IRCClient.userKicked} is called when someone else gets kicked. """ # Fight! - self.client.irc_KICK('Svadilfari!~svadi@yok.utu.fi', - ['#python', 'WOLF', 'shoryuken!']) - self.client.irc_KICK(self.user, - [self.channel, 'Svadilfari', 'hadouken!']) - self.assertEqual(self.client.methods, - [('kickedFrom', - ('#python', 'Svadilfari', 'shoryuken!')), - ('userKicked', - ('Svadilfari', self.channel, 'Wolf', 'hadouken!'))]) - + self.client.irc_KICK( + "Svadilfari!~svadi@yok.utu.fi", ["#python", "WOLF", "shoryuken!"] + ) + self.client.irc_KICK(self.user, [self.channel, "Svadilfari", "hadouken!"]) + self.assertEqual( + self.client.methods, + [ + ("kickedFrom", ("#python", "Svadilfari", "shoryuken!")), + ("userKicked", ("Svadilfari", self.channel, "Wolf", "hadouken!")), + ], + ) def test_irc_TOPIC(self): """ L{IRCClient.topicUpdated} is called when someone sets the topic. """ - self.client.irc_TOPIC(self.user, - [self.channel, 'new topic is new']) - self.assertEqual(self.client.methods, - [('topicUpdated', - ('Wolf', self.channel, 'new topic is new'))]) - + self.client.irc_TOPIC(self.user, [self.channel, "new topic is new"]) + self.assertEqual( + self.client.methods, + [("topicUpdated", ("Wolf", self.channel, "new topic is new"))], + ) def test_irc_RPL_TOPIC(self): """ L{IRCClient.topicUpdated} is called when the topic is initially reported. """ - self.client.irc_RPL_TOPIC(self.user, - ['?', self.channel, 'new topic is new']) - self.assertEqual(self.client.methods, - [('topicUpdated', - ('Wolf', self.channel, 'new topic is new'))]) - + self.client.irc_RPL_TOPIC(self.user, ["?", self.channel, "new topic is new"]) + self.assertEqual( + self.client.methods, + [("topicUpdated", ("Wolf", self.channel, "new topic is new"))], + ) def test_irc_RPL_NOTOPIC(self): """ L{IRCClient.topicUpdated} is called when the topic is removed. """ - self.client.irc_RPL_NOTOPIC(self.user, ['?', self.channel]) - self.assertEqual(self.client.methods, - [('topicUpdated', ('Wolf', self.channel, ''))]) - + self.client.irc_RPL_NOTOPIC(self.user, ["?", self.channel]) + self.assertEqual( + self.client.methods, [("topicUpdated", ("Wolf", self.channel, ""))] + ) class CTCPQueryTests(IRCTestCase): """ Tests for the C{ctcpQuery_*} methods. """ + def setUp(self): - self.user = 'Wolf!~wolf@yok.utu.fi' - self.channel = '#twisted' - self.client = CollectorClient(['ctcpMakeReply']) - + self.user = "Wolf!~wolf@yok.utu.fi" + self.channel = "#twisted" + self.client = CollectorClient(["ctcpMakeReply"]) def test_ctcpQuery_PING(self): """ L{IRCClient.ctcpQuery_PING} calls L{IRCClient.ctcpMakeReply} with the correct args. """ - self.client.ctcpQuery_PING(self.user, self.channel, 'data') - self.assertEqual(self.client.methods, - [('ctcpMakeReply', ('Wolf', [('PING', 'data')]))]) - + self.client.ctcpQuery_PING(self.user, self.channel, "data") + self.assertEqual( + self.client.methods, [("ctcpMakeReply", ("Wolf", [("PING", "data")]))] + ) def test_ctcpQuery_FINGER(self): """ L{IRCClient.ctcpQuery_FINGER} calls L{IRCClient.ctcpMakeReply} with the correct args. """ - self.client.fingerReply = 'reply' - self.client.ctcpQuery_FINGER(self.user, self.channel, 'data') - self.assertEqual(self.client.methods, - [('ctcpMakeReply', ('Wolf', [('FINGER', 'reply')]))]) - + self.client.fingerReply = "reply" + self.client.ctcpQuery_FINGER(self.user, self.channel, "data") + self.assertEqual( + self.client.methods, [("ctcpMakeReply", ("Wolf", [("FINGER", "reply")]))] + ) def test_ctcpQuery_SOURCE(self): """ L{IRCClient.ctcpQuery_SOURCE} calls L{IRCClient.ctcpMakeReply} with the correct args. """ - self.client.sourceURL = 'url' - self.client.ctcpQuery_SOURCE(self.user, self.channel, 'data') - self.assertEqual(self.client.methods, - [('ctcpMakeReply', ('Wolf', [('SOURCE', 'url'), - ('SOURCE', None)]))]) - + self.client.sourceURL = "url" + self.client.ctcpQuery_SOURCE(self.user, self.channel, "data") + self.assertEqual( + self.client.methods, + [("ctcpMakeReply", ("Wolf", [("SOURCE", "url"), ("SOURCE", None)]))], + ) def test_ctcpQuery_USERINFO(self): """ L{IRCClient.ctcpQuery_USERINFO} calls L{IRCClient.ctcpMakeReply} with the correct args. """ - self.client.userinfo = 'info' - self.client.ctcpQuery_USERINFO(self.user, self.channel, 'data') - self.assertEqual(self.client.methods, - [('ctcpMakeReply', ('Wolf', [('USERINFO', 'info')]))]) - + self.client.userinfo = "info" + self.client.ctcpQuery_USERINFO(self.user, self.channel, "data") + self.assertEqual( + self.client.methods, [("ctcpMakeReply", ("Wolf", [("USERINFO", "info")]))] + ) def test_ctcpQuery_CLIENTINFO(self): """ L{IRCClient.ctcpQuery_CLIENTINFO} calls L{IRCClient.ctcpMakeReply} with the correct args. """ - self.client.ctcpQuery_CLIENTINFO(self.user, self.channel, '') - self.client.ctcpQuery_CLIENTINFO(self.user, self.channel, 'PING PONG') - info = ('ACTION CLIENTINFO DCC ERRMSG FINGER PING SOURCE TIME ' - 'USERINFO VERSION') - self.assertEqual(self.client.methods, - [('ctcpMakeReply', ('Wolf', [('CLIENTINFO', info)])), - ('ctcpMakeReply', ('Wolf', [('CLIENTINFO', None)]))]) - + self.client.ctcpQuery_CLIENTINFO(self.user, self.channel, "") + self.client.ctcpQuery_CLIENTINFO(self.user, self.channel, "PING PONG") + info = ( + "ACTION CLIENTINFO DCC ERRMSG FINGER PING SOURCE TIME " "USERINFO VERSION" + ) + self.assertEqual( + self.client.methods, + [ + ("ctcpMakeReply", ("Wolf", [("CLIENTINFO", info)])), + ("ctcpMakeReply", ("Wolf", [("CLIENTINFO", None)])), + ], + ) def test_ctcpQuery_TIME(self): """ L{IRCClient.ctcpQuery_TIME} calls L{IRCClient.ctcpMakeReply} with the correct args. """ - self.client.ctcpQuery_TIME(self.user, self.channel, 'data') - self.assertEqual(self.client.methods[0][1][0], 'Wolf') - + self.client.ctcpQuery_TIME(self.user, self.channel, "data") + self.assertEqual(self.client.methods[0][1][0], "Wolf") def test_ctcpQuery_DCC(self): """ L{IRCClient.ctcpQuery_DCC} calls L{IRCClient.ctcpMakeReply} with the correct args. """ - self.client.ctcpQuery_DCC(self.user, self.channel, 'data') - self.assertEqual(self.client.methods, - [('ctcpMakeReply', - ('Wolf', [('ERRMSG', - "DCC data :Unknown DCC type 'DATA'")]))]) - + self.client.ctcpQuery_DCC(self.user, self.channel, "data") + self.assertEqual( + self.client.methods, + [ + ( + "ctcpMakeReply", + ("Wolf", [("ERRMSG", "DCC data :Unknown DCC type 'DATA'")]), + ) + ], + ) class DccChatFactoryTests(IRCTestCase): """ Tests for L{DccChatFactory}. """ + def test_buildProtocol(self): """ An instance of the L{irc.DccChat} protocol is returned, which has the factory property set to the factory which created it. """ - queryData = ('fromUser', None, None) + queryData = ("fromUser", None, None) factory = irc.DccChatFactory(None, queryData) - protocol = factory.buildProtocol('127.0.0.1') + protocol = factory.buildProtocol("127.0.0.1") self.assertIsInstance(protocol, irc.DccChat) self.assertEqual(protocol.factory, factory) - class DccDescribeTests(IRCTestCase): """ Tests for L{dccDescribe}. """ + def test_address(self): """ L{irc.dccDescribe} supports long IP addresses. """ - result = irc.dccDescribe('CHAT arg 3232235522 6666') + result = irc.dccDescribe("CHAT arg 3232235522 6666") self.assertEqual(result, "CHAT for host 192.168.0.2, port 6666") - class DccFileReceiveTests(IRCTestCase): """ Tests for L{DccFileReceive}. """ - def makeConnectedDccFileReceive(self, filename, resumeOffset=0, - overwrite=None): + + def makeConnectedDccFileReceive(self, filename, resumeOffset=0, overwrite=None): """ Factory helper that returns a L{DccFileReceive} instance for a specific test case. @param filename: Path to the local file where received data is stored. @@ -2868,11 +2737,10 @@ protocol.set_overwrite(True) transport = StringTransport() protocol.makeConnection(transport) return protocol - def allDataReceivedForProtocol(self, protocol, data): """ Arrange the protocol so that it received all data. @param protocol: The protocol which will receive the data. @@ -2882,82 +2750,76 @@ @type data: L{bytest} """ protocol.dataReceived(data) protocol.connectionLost(None) - def test_resumeFromResumeOffset(self): """ If given a resumeOffset argument, L{DccFileReceive} will attempt to resume from that number of bytes if the file exists. """ fp = FilePath(self.mktemp()) - fp.setContent(b'Twisted is awesome!') + fp.setContent(b"Twisted is awesome!") protocol = self.makeConnectedDccFileReceive(fp.path, resumeOffset=11) - self.allDataReceivedForProtocol(protocol, b'amazing!') - - self.assertEqual(fp.getContent(), b'Twisted is amazing!') - + self.allDataReceivedForProtocol(protocol, b"amazing!") + + self.assertEqual(fp.getContent(), b"Twisted is amazing!") def test_resumeFromResumeOffsetInTheMiddleOfAlreadyWrittenData(self): """ When resuming from an offset somewhere in the middle of the file, for example, if there are 50 bytes in a file, and L{DccFileReceive} is given a resumeOffset of 25, and after that 15 more bytes are written to the file, then the resultant file should have just 40 bytes of data. """ fp = FilePath(self.mktemp()) - fp.setContent(b'Twisted is amazing!') + fp.setContent(b"Twisted is amazing!") protocol = self.makeConnectedDccFileReceive(fp.path, resumeOffset=11) - self.allDataReceivedForProtocol(protocol, b'cool!') - - self.assertEqual(fp.getContent(), b'Twisted is cool!') - + self.allDataReceivedForProtocol(protocol, b"cool!") + + self.assertEqual(fp.getContent(), b"Twisted is cool!") def test_setOverwrite(self): """ When local file already exists it can be overwritten using the L{DccFileReceive.set_overwrite} method. """ fp = FilePath(self.mktemp()) - fp.setContent(b'I love contributing to Twisted!') + fp.setContent(b"I love contributing to Twisted!") protocol = self.makeConnectedDccFileReceive(fp.path, overwrite=True) - self.allDataReceivedForProtocol(protocol, b'Twisted rocks!') - - self.assertEqual(fp.getContent(), b'Twisted rocks!') - + self.allDataReceivedForProtocol(protocol, b"Twisted rocks!") + + self.assertEqual(fp.getContent(), b"Twisted rocks!") def test_fileDoesNotExist(self): """ If the file does not already exist, then L{DccFileReceive} will create one and write the data to it. """ fp = FilePath(self.mktemp()) protocol = self.makeConnectedDccFileReceive(fp.path) - self.allDataReceivedForProtocol(protocol, b'I <3 Twisted') - - self.assertEqual(fp.getContent(), b'I <3 Twisted') - + self.allDataReceivedForProtocol(protocol, b"I <3 Twisted") + + self.assertEqual(fp.getContent(), b"I <3 Twisted") def test_resumeWhenFileDoesNotExist(self): """ If given a resumeOffset to resume writing to a file that does not exist, L{DccFileReceive} will raise L{OSError}. """ fp = FilePath(self.mktemp()) error = self.assertRaises( - OSError, - self.makeConnectedDccFileReceive, fp.path, resumeOffset=1) + OSError, self.makeConnectedDccFileReceive, fp.path, resumeOffset=1 + ) self.assertEqual(errno.ENOENT, error.errno) - def test_fileAlreadyExistsNoOverwrite(self): """ If the file already exists and overwrite action was not asked, L{OSError} is raised. @@ -2965,13 +2827,12 @@ fp = FilePath(self.mktemp()) fp.touch() self.assertRaises(OSError, self.makeConnectedDccFileReceive, fp.path) - def test_failToOpenLocalFile(self): """ L{IOError} is raised when failing to open the requested path. """ - fp = FilePath(self.mktemp()).child(u'child-with-no-existing-parent') + fp = FilePath(self.mktemp()).child("child-with-no-existing-parent") self.assertRaises(IOError, self.makeConnectedDccFileReceive, fp.path) --- first pass +++ second pass @@ -1748,20 +1748,23 @@ False, 12, timestamp, ["#fakeusers", "#fakemisc"], ) - expected = "\r\n".join( - [ - ":%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User", - ":%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server", - ":%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time", - ":%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc", - ":%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.", - "", - ] - ) % dict(hostname=hostname, timestamp=timestamp, req=req, targ=targ) + expected = ( + "\r\n".join( + [ + ":%(hostname)s 311 %(req)s %(targ)s target host.com * :Target User", + ":%(hostname)s 312 %(req)s %(targ)s irc.host.com :A fake server", + ":%(hostname)s 317 %(req)s %(targ)s 12 %(timestamp)s :seconds idle, signon time", + ":%(hostname)s 319 %(req)s %(targ)s :#fakeusers #fakemisc", + ":%(hostname)s 318 %(req)s %(targ)s :End of WHOIS list.", + "", + ] + ) + % dict(hostname=hostname, timestamp=timestamp, req=req, targ=targ) + ) self.check(expected) class DummyClient(irc.IRCClient): """ ```
CyrilWaechter commented 4 years ago

I have another case if you like : blk_8l64bulm.log Workaround using --fast worked.

kkroening commented 4 years ago

Another example...

Source:

aaaaaaaaaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbbbbbbb(  # ccccccccccccccccccccccccccccccccccc
    d=0
)

Log:

Mode(target_versions={<TargetVersion.PY37: 7>}, line_length=88, string_normalization=False, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,3 +1,3 @@
-aaaaaaaaaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbbbbbbb(  # ccccccccccccccccccccccccccccccccccc
-    d=0
+aaaaaaaaaaaaaaaaaaaaaaaaaa = (
+    bbbbbbbbbbbbbbbbbbbb(d=0)  # ccccccccccccccccccccccccccccccccccc
 )
--- first pass
+++ second pass
@@ -1,3 +1,3 @@
-aaaaaaaaaaaaaaaaaaaaaaaaaa = (
-    bbbbbbbbbbbbbbbbbbbb(d=0)  # ccccccccccccccccccccccccccccccccccc
-)
+aaaaaaaaaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbbbbbbb(
+    d=0
+)  # ccccccccccccccccccccccccccccccccccc

Note: The error occurs if a, b, and c are longer or shorter, so long as the line is >88 characters in total.

niranjan94 commented 4 years ago

Got this error today.

Version: black, version 20.8b1

Mode(target_versions={<TargetVersion.PY38: 8>}, line_length=120, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -7,34 +7,34 @@

 def test_serialize_to_json():
     """Test if a dictionary can be serialized to json string correctly."""
     uuid_value = uuid.uuid4()
     assert (
-            serialize_to_json(
-                {
-                    "string": "hello world",
-                    "decimal": Decimal(3.14159),
-                    "list": [1, 2, 3, 3.14159, Decimal(3.14159)],
-                    "uuid": uuid_value,
-                    "date": datetime(2020, 1, 1, 1, 1, 1),
-                    "nested_dict": {"boolean": True, "float": "3.14159", "tuple": (1, 2, 3)},
-                }
-            )
-            == '{"string":"hello '
-               'world",'
-               '"decimal":3.14158999999999988261834005243144929409027099609375,'
-               '"list":[1,2,3,3.14159,3.14158999999999988261834005243144929409027099609375],'
-               '"uuid":"' + str(uuid_value) + '","date":"2020-01-01T01:01:01+00:00",'
-                                              '"nested_dict":{"boolean":true,"float":"3.14159","tuple":[1,2,3]}}'
+        serialize_to_json(
+            {
+                "string": "hello world",
+                "decimal": Decimal(3.14159),
+                "list": [1, 2, 3, 3.14159, Decimal(3.14159)],
+                "uuid": uuid_value,
+                "date": datetime(2020, 1, 1, 1, 1, 1),
+                "nested_dict": {"boolean": True, "float": "3.14159", "tuple": (1, 2, 3)},
+            }
+        )
+        == '{"string":"hello '
+        'world",'
+        '"decimal":3.14158999999999988261834005243144929409027099609375,'
+        '"list":[1,2,3,3.14159,3.14158999999999988261834005243144929409027099609375],'
+        '"uuid":"' + str(uuid_value) + '","date":"2020-01-01T01:01:01+00:00",'
+        '"nested_dict":{"boolean":true,"float":"3.14159","tuple":[1,2,3]}}'
     )

 def test_deserialize_from_json():
     """Test if a json string can be deserialized to a dictionary correctly."""
     assert (
-            deserialize_from_json(
-                """
+        deserialize_from_json(
+            """
                 {
                   "string": "hello world",
                   "decimal": 3.14158999999999988261834005243144929409027099609375,
                   "list": [
                     1,
@@ -54,18 +54,19 @@
                       3
                     ]
                   }
                 }
                 """
-            ) == {
-                'date': datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc),
-                'decimal': 3.14159,
-                'list': [1, 2, 3, 3.14159, 3.14159],
-                'nested_dict': {'boolean': True, 'float': '3.14159', 'tuple': [1, 2, 3]},
-                'string': 'hello world',
-                'uuid': '424984fc-485a-4063-8625-aea10a899ff5'
-            }
+        )
+        == {
+            "date": datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc),
+            "decimal": 3.14159,
+            "list": [1, 2, 3, 3.14159, 3.14159],
+            "nested_dict": {"boolean": True, "float": "3.14159", "tuple": [1, 2, 3]},
+            "string": "hello world",
+            "uuid": "424984fc-485a-4063-8625-aea10a899ff5",
+        }
     )

 def test_serialize_to_dict():
     """Test if a dictionary can be normalized to a serializable dictionary."""
--- first pass
+++ second pass
@@ -28,13 +28,12 @@
     )

 def test_deserialize_from_json():
     """Test if a json string can be deserialized to a dictionary correctly."""
-    assert (
-        deserialize_from_json(
-            """
+    assert deserialize_from_json(
+        """
                 {
                   "string": "hello world",
                   "decimal": 3.14158999999999988261834005243144929409027099609375,
                   "list": [
                     1,
@@ -54,19 +53,17 @@
                       3
                     ]
                   }
                 }
                 """
-        )
-        == {
-            "date": datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc),
-            "decimal": 3.14159,
-            "list": [1, 2, 3, 3.14159, 3.14159],
-            "nested_dict": {"boolean": True, "float": "3.14159", "tuple": [1, 2, 3]},
-            "string": "hello world",
-            "uuid": "424984fc-485a-4063-8625-aea10a899ff5",
-        }
-    )
+    ) == {
+        "date": datetime(2020, 1, 1, 1, 1, 1, tzinfo=timezone.utc),
+        "decimal": 3.14159,
+        "list": [1, 2, 3, 3.14159, 3.14159],
+        "nested_dict": {"boolean": True, "float": "3.14159", "tuple": [1, 2, 3]},
+        "string": "hello world",
+        "uuid": "424984fc-485a-4063-8625-aea10a899ff5",
+    }

 def test_serialize_to_dict():
     """Test if a dictionary can be normalized to a serializable dictionary."""
jeanas commented 4 years ago
def f():
    return a(
        b(
          c(n)
         #
         ),
        []
     ) + d(x, y)

gives this:

Mode(target_versions={<TargetVersion.PY36: 6>, <TargetVersion.PY38: 8>, <TargetVersion.PY37: 7>}, line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,9 +1,8 @@
 def f():
     return a(
         b(
-          c(n)
-         #
-         ),
-        []
-     ) + d(x, y)
-
+            c(n)
+            #
+        ),
+        [],
+    ) + d(x, y)
--- first pass
+++ second pass
@@ -1,8 +1,11 @@
 def f():
-    return a(
-        b(
-            c(n)
-            #
-        ),
-        [],
-    ) + d(x, y)
+    return (
+        a(
+            b(
+                c(n)
+                #
+            ),
+            [],
+        )
+        + d(x, y)
+    )

This happens with Black 20.8b1, but not 19.3b0. The first bad commit is 586d24236e6b57bc3b5da85fdbe2563835021076.

felixxm commented 4 years ago

We have the same issue in Django with version 20.8b1: blk_j65piel1.log

jerinpetergeorge commented 4 years ago

Version 20.8b1 Log: blk_g8be276f.log

MinchinWeb commented 4 years ago

I don't have the diffs, but here are three segments that broke black. Version 20.8b1

import numpy as np
from metpy.testing import assert_almost_equal
from metpy.calc import height_to_geopotential
from metpy.units import units

def test_height_to_geopotential_32bit():
    """Test conversion to geopotential with 32-bit values."""
    heights = np.linspace(20597, 20598, 11, dtype=np.float32) * units.m
    truth = np.array([201336.67, 201337.66, 201338.62, 201339.61, 201340.58, 201341.56,
                      201342.53, 201343.52, 201344.48, 201345.44, 201346.42],
                     dtype=np.float32) * units('J/kg')
    assert_almost_equal(height_to_geopotential(heights), truth, 2)
import numpy as np
import pytest
from metpy.testing import assert_array_almost_equal
from metpy.calc import get_layer

@pytest.mark.parametrize('flip_order', [(True, False)])
def test_get_layer_float32(flip_order):
    """Test that get_layer works properly with float32 data."""
    p = np.asarray([940.85083008, 923.78851318, 911.42022705, 896.07220459,
                    876.89404297, 781.63330078], np.float32) * units('hPa')
    hgt = np.asarray([563.671875, 700.93817139, 806.88098145, 938.51745605,
                      1105.25854492, 2075.04443359], dtype=np.float32) * units.meter

    true_p_layer = np.asarray([940.85083008, 923.78851318, 911.42022705, 896.07220459,
                               876.89404297, 831.86472819], np.float32) * units('hPa')
    true_hgt_layer = np.asarray([563.671875, 700.93817139, 806.88098145, 938.51745605,
                                 1105.25854492, 1549.8079], dtype=np.float32) * units.meter

    if flip_order:
        p = p[::-1]
        hgt = hgt[::-1]
    p_layer, hgt_layer = get_layer(p, hgt, height=hgt, depth=1000. * units.meter)
    assert_array_almost_equal(p_layer, true_p_layer, 4)
    assert_array_almost_equal(hgt_layer, true_hgt_layer, 4)
from metpy.xarray import grid_deltas_from_dataarray
from metpy.calc import first_derivative
import numpy as np
from metpy.testing import assert_array_almost_equal
from metpy.units import units

def test_first_derivative_xarray_pint_conversion(test_da_lonlat):
    """Test first derivative with implicit xarray to pint quantity conversion."""
    dx, _ = grid_deltas_from_dataarray(test_da_lonlat)
    deriv = first_derivative(test_da_lonlat, delta=dx, axis=-1)
    truth = np.array([[[-3.30782978e-06] * 4, [-3.42816074e-06] * 4, [-3.57012948e-06] * 4,
                       [-3.73759364e-06] * 4]] * 3) * units('kelvin / meter')
    assert_array_almost_equal(deriv, truth, 12)
yohanboniface commented 4 years ago

Here is a minimal example to reproduce the issue, with black version 20.8b1 (works with version 19.10b0)

def test_foo():
    assert foo(
        "foo",
    ) == [{"raw": {"person": "1"}, "error": "Invalid field unknown", "status": "error"}]

If I remove one char from last line, the issue disappears.

diff ```diff Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False) --- source +++ first pass @@ -1,4 +1,13 @@ def test_foo(): - assert foo( - "foo", - ) == [{"raw": {"person": "1"}, "error": "Invalid field unknown", "status": "error"}] + assert ( + foo( + "foo", + ) + == [ + { + "raw": {"person": "1"}, + "error": "Invalid field unknown", + "status": "error", + } + ] + ) --- first pass +++ second pass @@ -1,13 +1,8 @@ def test_foo(): - assert ( - foo( - "foo", - ) - == [ - { - "raw": {"person": "1"}, - "error": "Invalid field unknown", - "status": "error", - } - ] - ) + assert foo("foo",) == [ + { + "raw": {"person": "1"}, + "error": "Invalid field unknown", + "status": "error", + } + ] ```
teake commented 3 years ago

Here's another example + log, run against black, version 20.8b2.dev31+gdd2f86a (master at the time of writing).

my_string = '______________ %s _________________ %s _____________________________________________' % (
    '____________', '________________', '___________________________________________')

blk_smkp2msa.log

frank690 commented 3 years ago

I just encountered the following on a unittest of mine. Version: 20.8b1 The assert statement in combination with sorted is modified twice.


Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -13,9 +13,12 @@
         ([(1, 2, 3), ("a", "b", "c")], [("a", "b", "c")], []),
         ([("a",)], [(1,), ("a",)], [(1,)]),
         ([], [(1,), ("a", "b")], [(1,), ("a", "b")]),
     ],
 )
-def test_compare_row_lists(items_to_remove, target_to_remove_items_from, expected_results):
-    assert sorted(compare_row_lists(basis=items_to_remove,
-                                    to_reduce=target_to_remove_items_from), key=str) == sorted(expected_results,
-                                                                                               key=str)
+def test_compare_row_lists(
+    items_to_remove, target_to_remove_items_from, expected_results
+):
+    assert sorted(
+        compare_row_lists(basis=items_to_remove, to_reduce=target_to_remove_items_from),
+        key=str,
+    ) == sorted(expected_results, key=str)
--- first pass
+++ second pass
@@ -16,9 +16,14 @@
     ],
 )
 def test_compare_row_lists(
     items_to_remove, target_to_remove_items_from, expected_results
 ):
-    assert sorted(
-        compare_row_lists(basis=items_to_remove, to_reduce=target_to_remove_items_from),
-        key=str,
-    ) == sorted(expected_results, key=str)
+    assert (
+        sorted(
+            compare_row_lists(
+                basis=items_to_remove, to_reduce=target_to_remove_items_from
+            ),
+            key=str,
+        )
+        == sorted(expected_results, key=str)
+    )```
hugobuddel commented 3 years ago

FYI, running --fast twice is not sufficient for many of the examples here, because a --diff will still create a different result. However, reducing the maximum line length by one in the first pass seems to always work.

Test source (created from examples here): blacktest.py

```python if True: if ( mission.get("status") not in [ "new", "test", "scheduled" ] and not mission.get("redeem") ): False if get_flag('WITH_PYMALLOC', lambda: impl == 'cp', warn=(impl == 'cp' and sys.version_info < (3, 8))) \ and sys.version_info < (3, 8): pass if get_flag('Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff, expected=4, warn=(impl == 'cp' and sys.version_info < (3, 3))) \ and sys.version_info < (3, 3): pass should_list_installed = ( subcommand_name in ['show', 'uninstall'] and not current.startswith('-') ) assert ( xxxxxx( xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx, ) == xxxxxx(xxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx) ) if any( k in t for k in ["AAAAAAAAAA", "AAAAA", "AAAAAA", "AAAAAAAAA", "AAA", "AAAAAA", "AAAAAAAA", "AAA", "AAAAA", "AAAAA", "AAAA"] ) and not any(k in t for k in ["AAA"]): pass aaaaaaaaaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbbbbbbb( # ccccccccccccccccccccccccccccccccccc d=0 ) def f(): return a( b( c(n) # ), [] ) + d(x, y) my_string = '______________ %s _________________ %s _____________________________________________' % ( '____________', '________________', '___________________________________________') ```

Black version:

$ black --version
black, version 20.8b1

This will fail:

$ black --fast blacktest.py
$ black --fast blacktest.py
$ black --diff blacktest.py
with diffoutput:

```diff --- blacktest.py 2020-10-19 18:27:45.116011 +0000 +++ blacktest.py 2020-10-19 18:28:33.654679 +0000 @@ -2,74 +2,87 @@ if mission.get("status") not in ["new", "test", "scheduled"] and not mission.get( "redeem" ): False -if get_flag( - "WITH_PYMALLOC", - lambda: impl == "cp", - warn=(impl == "cp" and sys.version_info < (3, 8)), -) and sys.version_info < (3, 8): +if ( + get_flag( + "WITH_PYMALLOC", + lambda: impl == "cp", + warn=(impl == "cp" and sys.version_info < (3, 8)), + ) + and sys.version_info < (3, 8) +): pass -if get_flag( - "Py_UNICODE_SIZE", - lambda: sys.maxunicode == 0x10FFFF, - expected=4, - warn=(impl == "cp" and sys.version_info < (3, 3)), -) and sys.version_info < (3, 3): +if ( + get_flag( + "Py_UNICODE_SIZE", + lambda: sys.maxunicode == 0x10FFFF, + expected=4, + warn=(impl == "cp" and sys.version_info < (3, 3)), + ) + and sys.version_info < (3, 3) +): pass -should_list_installed = subcommand_name in [ - "show", - "uninstall", -] and not current.startswith("-") +should_list_installed = ( + subcommand_name + in [ + "show", + "uninstall", + ] + and not current.startswith("-") +) assert ( xxxxxx( xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx, ) == xxxxxx(xxxxxxxxxxx, xxxxxxxxxxxxxxxxxxxxxxxxx) ) -if any( - k in t - for k in [ - "AAAAAAAAAA", - "AAAAA", - "AAAAAA", - "AAAAAAAAA", - "AAA", - "AAAAAA", - "AAAAAAAA", - "AAA", - "AAAAA", - "AAAAA", - "AAAA", - ] -) and not any(k in t for k in ["AAA"]): +if ( + any( + k in t + for k in [ + "AAAAAAAAAA", + "AAAAA", + "AAAAAA", + "AAAAAAAAA", + "AAA", + "AAAAAA", + "AAAAAAAA", + "AAA", + "AAAAA", + "AAAAA", + "AAAA", + ] + ) + and not any(k in t for k in ["AAA"]) +): pass -aaaaaaaaaaaaaaaaaaaaaaaaaa = ( - bbbbbbbbbbbbbbbbbbbb(d=0) # ccccccccccccccccccccccccccccccccccc -) +aaaaaaaaaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbbbbbbb( + d=0 +) # ccccccccccccccccccccccccccccccccccc def f(): - return a( - b( - c(n) - # - ), - [], - ) + d(x, y) + return ( + a( + b( + c(n) + # + ), + [], + ) + + d(x, y) + ) -my_string = ( - "______________ %s _________________ %s _____________________________________________" - % ( - "____________", - "________________", - "___________________________________________", - ) +my_string = "______________ %s _________________ %s _____________________________________________" % ( + "____________", + "________________", + "___________________________________________", ) ```

However, this will work fine:

$ black --fast -l 87 blacktest.py 
reformatted blacktest.py
All done! ✨ 🍰 ✨
1 file reformatted.
$ black --fast blacktest.py 
reformatted blacktest.py
All done! ✨ 🍰 ✨
1 file reformatted.
$ black --diff blacktest.py 
All done! ✨ 🍰 ✨
1 file would be left unchanged.

So for a proper workaround, do the first --fast black with -l 87!

eli88fine commented 3 years ago

Running Black 20.8b1

Log: Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False) --- source +++ first pass @@ -57,12 +57,12 @@ base_path: If left blank, this will default to the path to the file that called this function. When frozen as EXE, this will always be the path to the Pyinstaller directory.

 For use specifically with files compiled to windows EXE by pyinstaller.
 """
 if base_path is None:
jclevine commented 3 years ago

blk_46i0c5cz.log

rpmanser commented 3 years ago

Running black 20.8b1. Passing the --fast flag solves this particular case.

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -11,10 +11,11 @@
 from metpy.units import units
 from metpy.testing import assert_almost_equal, assert_array_almost_equal

 from hums.constants import AREA_ROOF, AREA_SHED, AREA_TANK
 from hums.water import HydroVu, tank_volume, forecast_depth, forecast_volume
+

 class TestHydroVu:
     @pytest.fixture(scope="session")
     def hydrovu(self):
         """Instantiate a HydroVu object to use in tests."""
@@ -155,26 +156,42 @@
         test_file.unlink()

 def test_tank_volume():
     """Test the tank_volume function"""
-    depth = np.array([1.1, 1.0, 1.1, 0.9, 1.0, 1.1, 1.0]) * units('meter')
-    truth = np.array([5039.41735, 4581.2885, 5039.41735, 4123.15965, 4581.2885, 5039.41735, 4581.2885]) * units('gallon')
+    depth = np.array([1.1, 1.0, 1.1, 0.9, 1.0, 1.1, 1.0]) * units("meter")
+    truth = np.array(
+        [
+            5039.41735,
+            4581.2885,
+            5039.41735,
+            4123.15965,
+            4581.2885,
+            5039.41735,
+            4581.2885,
+        ]
+    ) * units("gallon")

     actual = tank_volume(depth)
     assert_array_almost_equal(actual, truth, decimal=6)

+
 def test_forecast_depth():
     """Test the forecast_depth function"""
-    precip_accum = np.array([0.05, 0.07, 0.07, 0.12, 0.34]) * units('inches')
-    truth = np.array([0.0286736, 0.04014298, 0.04014298, 0.0688165, 0.1949802]) * units('feet')
+    precip_accum = np.array([0.05, 0.07, 0.07, 0.12, 0.34]) * units("inches")
+    truth = np.array([0.0286736, 0.04014298, 0.04014298, 0.0688165, 0.1949802]) * units(
+        "feet"
+    )

     actual = forecast_depth(precip_accum)
     assert_array_almost_equal(actual, truth)

+
 def test_forecast_volume():
     """Test the forecast_volume function"""
-    precip_accum = np.array([0.05, 0.07, 0.07, 0.12, 0.34]) * units('inches')
-    truth = np.array([40.0392738, 56.0548996, 56.0548996, 96.0940617, 272.266671]) * units('gallon')
+    precip_accum = np.array([0.05, 0.07, 0.07, 0.12, 0.34]) * units("inches")
+    truth = np.array(
+        [40.0392738, 56.0548996, 56.0548996, 96.0940617, 272.266671]
+    ) * units("gallon")

     actual = forecast_volume(precip_accum)
     assert_array_almost_equal(actual, truth, decimal=3)
--- first pass
+++ second pass
@@ -157,21 +157,24 @@

 def test_tank_volume():
     """Test the tank_volume function"""
     depth = np.array([1.1, 1.0, 1.1, 0.9, 1.0, 1.1, 1.0]) * units("meter")
-    truth = np.array(
-        [
-            5039.41735,
-            4581.2885,
-            5039.41735,
-            4123.15965,
-            4581.2885,
-            5039.41735,
-            4581.2885,
-        ]
-    ) * units("gallon")
+    truth = (
+        np.array(
+            [
+                5039.41735,
+                4581.2885,
+                5039.41735,
+                4123.15965,
+                4581.2885,
+                5039.41735,
+                4581.2885,
+            ]
+        )
+        * units("gallon")
+    )

     actual = tank_volume(depth)
     assert_array_almost_equal(actual, truth, decimal=6)
Chadys commented 3 years ago

Version 20.8b1

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -3,12 +3,14 @@

 class TestConvertPBX:
     def test_process_single_row(self):
         convert_pbx_command = Command()

-        assert convert_pbx_command.process_single_row(3, {"SETUP": "2012-03-08 blabla",
-                                                           "DESTINATION": "+666",
-                                                           "OPERATOR": '<name>.pbx.dummy.com',
-                                                           "DURATION (Seconds)": 456.7}) == ("2012-03-08",
-                                                                                             "666", "<name>",
-                                                                                             456.7, 3)
-
+        assert convert_pbx_command.process_single_row(
+            3,
+            {
+                "SETUP": "2012-03-08 blabla",
+                "DESTINATION": "+666",
+                "OPERATOR": "<name>.pbx.dummy.com",
+                "DURATION (Seconds)": 456.7,
+            },
+        ) == ("2012-03-08", "666", "<name>", 456.7, 3)
--- first pass
+++ second pass
@@ -3,14 +3,17 @@

 class TestConvertPBX:
     def test_process_single_row(self):
         convert_pbx_command = Command()

-        assert convert_pbx_command.process_single_row(
-            3,
-            {
-                "SETUP": "2012-03-08 blabla",
-                "DESTINATION": "+666",
-                "OPERATOR": "<name>.pbx.dummy.com",
-                "DURATION (Seconds)": 456.7,
-            },
-        ) == ("2012-03-08", "666", "<name>", 456.7, 3)
+        assert (
+            convert_pbx_command.process_single_row(
+                3,
+                {
+                    "SETUP": "2012-03-08 blabla",
+                    "DESTINATION": "+666",
+                    "OPERATOR": "<name>.pbx.dummy.com",
+                    "DURATION (Seconds)": 456.7,
+                },
+            )
+            == ("2012-03-08", "666", "<name>", 456.7, 3)
+        )
jonashaag commented 3 years ago

Mode(target_versions={<TargetVersion.PY36: 6>}, line_length=100, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass

 def itu_r_468_weighted_torch(spec, n_fft, sr):
     assert spec.ndim == 3
     assert spec.shape[-1] == n_fft // 2 + 1
-    return spec * torch.tensor([
-        itu_r_468_weighting.filter.r468(f, "1khz", "factor")
-        for f in librosa.fft_frequencies(sr, n_fft)], device=spec.device)[None, None]
+    return (
+        spec
+        * torch.tensor(
+            [
+                itu_r_468_weighting.filter.r468(f, "1khz", "factor")
+                for f in librosa.fft_frequencies(sr, n_fft)
+            ],
+            device=spec.device,
+        )[None, None]
+    )

 def rand_shelv(rand, sr, min_cutoff, max_cutoff, min_q, max_q, min_g, max_g, t, data):
     f = rand.uniform(min_cutoff, max_cutoff)
     q = rand.uniform(min_q, max_q)

--- first pass
+++ second pass
@@ -395,20 +395,17 @@

 def itu_r_468_weighted_torch(spec, n_fft, sr):
     assert spec.ndim == 3
     assert spec.shape[-1] == n_fft // 2 + 1
-    return (
-        spec
-        * torch.tensor(
-            [
-                itu_r_468_weighting.filter.r468(f, "1khz", "factor")
-                for f in librosa.fft_frequencies(sr, n_fft)
-            ],
-            device=spec.device,
-        )[None, None]
-    )
+    return spec * torch.tensor(
+        [
+            itu_r_468_weighting.filter.r468(f, "1khz", "factor")
+            for f in librosa.fft_frequencies(sr, n_fft)
+        ],
+        device=spec.device,
+    )[None, None]

 def rand_shelv(rand, sr, min_cutoff, max_cutoff, min_q, max_q, min_g, max_g, t, data):
     f = rand.uniform(min_cutoff, max_cutoff)
     q = rand.uniform(min_q, max_q)
MahvishSC commented 3 years ago

Encountered this today running black, version 20.8b1 (first time setup) on Mac error: cannot format reponame/filename.py: INTERNAL ERROR: Black produced different code on the second pass of the formatter. Please report a bug on https://github.com/psf/black/issues. This diff might be helpful: /var/folders/6t/my1ympps5vl3k1538gk9n_1h0000gp/T/blk__xaj_7vv.log Oh no! 💥 💔 💥 1 file failed to reformat.

e2jk commented 3 years ago

Another example, if it helps. Version 20.8b1. The line causing this issue for me:

if output_format in ("Integer", "Decimal", "Keep numeric", "Time") or output_format.startswith("Date ("):

Attached log indicates that first pass produces

if output_format in (
    "Integer",
    "Decimal",
    "Keep numeric",
    "Time",
) or output_format.startswith("Date ("):

but that the second pass produces

if (
    output_format
    in (
        "Integer",
        "Decimal",
        "Keep numeric",
        "Time",
    )
    or output_format.startswith("Date (")
):

When manually changing the code to the value generated by the first pass, black doesn't complain anymore and indeed generates the code from the second pass.

fixator10 commented 3 years ago
Mode(target_versions=set(), line_length=99, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -117,14 +117,16 @@
                     emb["fields"].append(field)

             # Cooldown handling
             if cd := command._buckets._cooldown:
                 per = _("per")
-                cooldown_text = (
-                    _("Can be used {} time(s) per {}").format(cd.rate, hum if (hum := chat.humanize_timedelta(seconds=cd.per)) else f"{int(cd.per)*1000}")
-                    + _(', per ' + cd.type.name if cd.type.name != 'default' else ' globally')
-                )
+                cooldown_text = _("Can be used {} time(s) per {}").format(
+                    cd.rate,
+                    hum
+                    if (hum := chat.humanize_timedelta(seconds=cd.per))
+                    else f"{int(cd.per)*1000}",
+                ) + _(", per " + cd.type.name if cd.type.name != "default" else " globally")
                 field_cooldown = commands.help.EmbedField(
                     _("**__Cooldown:__**"), cooldown_text, False
                 )

                 emb["fields"].append(field_cooldown)
--- first pass
+++ second pass
@@ -117,16 +117,19 @@
                     emb["fields"].append(field)

             # Cooldown handling
             if cd := command._buckets._cooldown:
                 per = _("per")
-                cooldown_text = _("Can be used {} time(s) per {}").format(
-                    cd.rate,
-                    hum
-                    if (hum := chat.humanize_timedelta(seconds=cd.per))
-                    else f"{int(cd.per)*1000}",
-                ) + _(", per " + cd.type.name if cd.type.name != "default" else " globally")
+                cooldown_text = (
+                    _("Can be used {} time(s) per {}").format(
+                        cd.rate,
+                        hum
+                        if (hum := chat.humanize_timedelta(seconds=cd.per))
+                        else f"{int(cd.per)*1000}",
+                    )
+                    + _(", per " + cd.type.name if cd.type.name != "default" else " globally")
+                )
                 field_cooldown = commands.help.EmbedField(
                     _("**__Cooldown:__**"), cooldown_text, False
                 )

                 emb["fields"].append(field_cooldown)
jnns commented 3 years ago

Black 20.8b1

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -61,11 +61,11 @@
         num = len(timeslots)

         message = ngettext(
             "%(num)s cart delivery object generated.",
             "%(num)s cart delivery objects generated.",
-            num
+            num,
         ) % {"num": num}

         self.message_user(request, message)

--- first pass
+++ second pass
@@ -58,15 +58,18 @@
     def generate(self, request):
         """Generate cart deliveries for existing specific timeslots."""
         timeslots = CartDelivery.objects.generate()
         num = len(timeslots)

-        message = ngettext(
-            "%(num)s cart delivery object generated.",
-            "%(num)s cart delivery objects generated.",
-            num,
-        ) % {"num": num}
+        message = (
+            ngettext(
+                "%(num)s cart delivery object generated.",
+                "%(num)s cart delivery objects generated.",
+                num,
+            )
+            % {"num": num}
+        )

         self.message_user(request, message)
ThatXliner commented 3 years ago

;-;

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,7 +1,12 @@
 from __future__ import absolute_import
-from lark.exceptions import UnexpectedCharacters, UnexpectedInput, UnexpectedToken, ConfigurationError
+from lark.exceptions import (
+    UnexpectedCharacters,
+    UnexpectedInput,
+    UnexpectedToken,
+    ConfigurationError,
+)

 import sys, os, pickle, hashlib
 from io import open
 import tempfile
 from warnings import warn
@@ -15,22 +20,22 @@
 from .parse_tree_builder import ParseTreeBuilder
 from .parser_frontends import get_frontend, _get_lexer_callbacks
 from .grammar import Rule

 import re
+
 try:
     import regex
 except ImportError:
     regex = None

 ###{standalone

 class LarkOptions(Serialize):
-    """Specifies the options for Lark
-
-    """
+    """Specifies the options for Lark"""
+
     OPTIONS_DOC = """
     **===  General Options  ===**

     start
             The start symbol. Either a string, or a list of strings for multiple possible starts (Default: "start")
@@ -102,67 +107,70 @@
     **=== End Options ===**
     """
     if __doc__:
         __doc__ += OPTIONS_DOC

-
     # Adding a new option needs to be done in multiple places:
     # - In the dictionary below. This is the primary truth of which options `Lark.__init__` accepts
     # - In the docstring above. It is used both for the docstring of `LarkOptions` and `Lark`, and in readthedocs
     # - In `lark-stubs/lark.pyi`:
     #   - As attribute to `LarkOptions`
     #   - As parameter to `Lark.__init__`
     # - Potentially in `_LOAD_ALLOWED_OPTIONS` below this class, when the option doesn't change how the grammar is loaded
     # - Potentially in `lark.tools.__init__`, if it makes sense, and it can easily be passed as a cmd argument
     _defaults = {
-        'debug': False,
-        'keep_all_tokens': False,
-        'tree_class': None,
-        'cache': False,
-        'postlex': None,
-        'parser': 'earley',
-        'lexer': 'auto',
-        'transformer': None,
-        'start': 'start',
-        'priority': 'auto',
-        'ambiguity': 'auto',
-        'regex': False,
-        'propagate_positions': False,
-        'lexer_callbacks': {},
-        'maybe_placeholders': False,
-        'edit_terminals': None,
-        'g_regex_flags': 0,
-        'use_bytes': False,
-        'import_paths': [],
-        'source_path': None,
+        "debug": False,
+        "keep_all_tokens": False,
+        "tree_class": None,
+        "cache": False,
+        "postlex": None,
+        "parser": "earley",
+        "lexer": "auto",
+        "transformer": None,
+        "start": "start",
+        "priority": "auto",
+        "ambiguity": "auto",
+        "regex": False,
+        "propagate_positions": False,
+        "lexer_callbacks": {},
+        "maybe_placeholders": False,
+        "edit_terminals": None,
+        "g_regex_flags": 0,
+        "use_bytes": False,
+        "import_paths": [],
+        "source_path": None,
     }

     def __init__(self, options_dict):
         o = dict(options_dict)

         options = {}
         for name, default in self._defaults.items():
             if name in o:
                 value = o.pop(name)
-                if isinstance(default, bool) and name not in ('cache', 'use_bytes'):
+                if isinstance(default, bool) and name not in ("cache", "use_bytes"):
                     value = bool(value)
             else:
                 value = default

             options[name] = value

-        if isinstance(options['start'], STRING_TYPE):
-            options['start'] = [options['start']]
-
-        self.__dict__['options'] = options
-
-        if not self.parser in ('earley', 'lalr', 'cyk', None):
-            raise ConfigurationError(f"{self.parser} must be one of {', '.join(('earley', 'lalr', 'cyk', None))}")
-
-        if self.parser == 'earley' and self.transformer:
-            raise ValueError('Cannot specify an embedded transformer when using the Earley algorithm.'
-                             'Please use your transformer on the resulting parse tree, or use a different algorithm (i.e. LALR)')
+        if isinstance(options["start"], STRING_TYPE):
+            options["start"] = [options["start"]]
+
+        self.__dict__["options"] = options
+
+        if not self.parser in ("earley", "lalr", "cyk", None):
+            raise ConfigurationError(
+                f"{self.parser} must be one of {', '.join(('earley', 'lalr', 'cyk', None))}"
+            )
+
+        if self.parser == "earley" and self.transformer:
+            raise ValueError(
+                "Cannot specify an embedded transformer when using the Earley algorithm."
+                "Please use your transformer on the resulting parse tree, or use a different algorithm (i.e. LALR)"
+            )

         if o:
             raise ValueError("Unknown options: %s" % o.keys())

     def __getattr__(self, name):
@@ -183,14 +191,23 @@
         return cls(data)

 # Options that can be passed to the Lark parser, even when it was loaded from cache/standalone.
 # These option are only used outside of `load_grammar`.
-_LOAD_ALLOWED_OPTIONS = {'postlex', 'transformer', 'use_bytes', 'debug', 'g_regex_flags', 'regex', 'propagate_positions', 'tree_class'}
-
-_VALID_PRIORITY_OPTIONS = ('auto', 'normal', 'invert', None)
-_VALID_AMBIGUITY_OPTIONS = ('auto', 'resolve', 'explicit', 'forest')
+_LOAD_ALLOWED_OPTIONS = {
+    "postlex",
+    "transformer",
+    "use_bytes",
+    "debug",
+    "g_regex_flags",
+    "regex",
+    "propagate_positions",
+    "tree_class",
+}
+
+_VALID_PRIORITY_OPTIONS = ("auto", "normal", "invert", None)
+_VALID_AMBIGUITY_OPTIONS = ("auto", "resolve", "explicit", "forest")

 class Lark(Serialize):
     """Main interface for the library.

@@ -202,29 +219,32 @@

     Example:
         >>> Lark(r'''start: "foo" ''')
         Lark(...)
     """
+
     def __init__(self, grammar, **options):
         self.options = LarkOptions(options)

         # Set regex or re module
         use_regex = self.options.regex
         if use_regex:
             if regex:
                 re_module = regex
             else:
-                raise ImportError('`regex` module must be installed if calling `Lark(regex=True)`.')
+                raise ImportError(
+                    "`regex` module must be installed if calling `Lark(regex=True)`."
+                )
         else:
             re_module = re

         # Some, but not all file-like objects have a 'name' attribute
         if self.options.source_path is None:
             try:
                 self.source_path = grammar.name
             except AttributeError:
-                self.source_path = '<string>'
+                self.source_path = "<string>"
         else:
             self.source_path = self.options.source_path

         # Drain file-like objects to get their contents
         try:
@@ -237,88 +257,119 @@
         assert isinstance(grammar, STRING_TYPE)
         self.source_grammar = grammar
         if self.options.use_bytes:
             if not isascii(grammar):
                 raise ValueError("Grammar must be ascii only, when use_bytes=True")
-            if sys.version_info[0] == 2 and self.options.use_bytes != 'force':
-                raise NotImplementedError("`use_bytes=True` may have issues on python2."
-                                          "Use `use_bytes='force'` to use it at your own risk.")
+            if sys.version_info[0] == 2 and self.options.use_bytes != "force":
+                raise NotImplementedError(
+                    "`use_bytes=True` may have issues on python2."
+                    "Use `use_bytes='force'` to use it at your own risk."
+                )

         cache_fn = None
         if self.options.cache:
-            if self.options.parser != 'lalr':
+            if self.options.parser != "lalr":
                 raise NotImplementedError("cache only works with parser='lalr' for now")
             if isinstance(self.options.cache, STRING_TYPE):
                 cache_fn = self.options.cache
             else:
                 if self.options.cache is not True:
                     raise ValueError("cache argument must be bool or str")
-                unhashable = ('transformer', 'postlex', 'lexer_callbacks', 'edit_terminals')
+                unhashable = (
+                    "transformer",
+                    "postlex",
+                    "lexer_callbacks",
+                    "edit_terminals",
+                )
                 from . import __version__
-                options_str = ''.join(k+str(v) for k, v in options.items() if k not in unhashable)
+
+                options_str = "".join(
+                    k + str(v) for k, v in options.items() if k not in unhashable
+                )
                 s = grammar + options_str + __version__
                 md5 = hashlib.md5(s.encode()).hexdigest()
-                cache_fn = tempfile.gettempdir() + '/.lark_cache_%s.tmp' % md5
+                cache_fn = tempfile.gettempdir() + "/.lark_cache_%s.tmp" % md5

             if FS.exists(cache_fn):
-                logger.debug('Loading grammar from cache: %s', cache_fn)
+                logger.debug("Loading grammar from cache: %s", cache_fn)
                 # Remove options that aren't relevant for loading from cache
-                for name in (set(options) - _LOAD_ALLOWED_OPTIONS):
+                for name in set(options) - _LOAD_ALLOWED_OPTIONS:
                     del options[name]
-                with FS.open(cache_fn, 'rb') as f:
+                with FS.open(cache_fn, "rb") as f:
                     self._load(f, **options)
                 return

-        if self.options.lexer == 'auto':
-            if self.options.parser == 'lalr':
-                self.options.lexer = 'contextual'
-            elif self.options.parser == 'earley':
-                self.options.lexer = 'dynamic'
-            elif self.options.parser == 'cyk':
-                self.options.lexer = 'standard'
+        if self.options.lexer == "auto":
+            if self.options.parser == "lalr":
+                self.options.lexer = "contextual"
+            elif self.options.parser == "earley":
+                self.options.lexer = "dynamic"
+            elif self.options.parser == "cyk":
+                self.options.lexer = "standard"
             else:
                 assert False, self.options.parser
         lexer = self.options.lexer
-        assert lexer in ('standard', 'contextual', 'dynamic', 'dynamic_complete') or issubclass(lexer, Lexer)
-
-        if self.options.ambiguity == 'auto':
-            if self.options.parser == 'earley':
-                self.options.ambiguity = 'resolve'
+        assert lexer in (
+            "standard",
+            "contextual",
+            "dynamic",
+            "dynamic_complete",
+        ) or issubclass(lexer, Lexer)
+
+        if self.options.ambiguity == "auto":
+            if self.options.parser == "earley":
+                self.options.ambiguity = "resolve"
         else:
-            disambig_parsers = ['earley', 'cyk']
+            disambig_parsers = ["earley", "cyk"]
             assert self.options.parser in disambig_parsers, (
-                'Only %s supports disambiguation right now') % ', '.join(disambig_parsers)
-
-        if self.options.priority == 'auto':
-            self.options.priority = 'normal'
+                "Only %s supports disambiguation right now"
+            ) % ", ".join(disambig_parsers)
+
+        if self.options.priority == "auto":
+            self.options.priority = "normal"

         if self.options.priority not in _VALID_PRIORITY_OPTIONS:
-            raise ValueError("invalid priority option: %r. Must be one of %r" % (self.options.priority, _VALID_PRIORITY_OPTIONS))
-        assert self.options.ambiguity not in ('resolve__antiscore_sum', ), 'resolve__antiscore_sum has been replaced with the option priority="invert"'
+            raise ValueError(
+                "invalid priority option: %r. Must be one of %r"
+                % (self.options.priority, _VALID_PRIORITY_OPTIONS)
+            )
+        assert self.options.ambiguity not in (
+            "resolve__antiscore_sum",
+        ), 'resolve__antiscore_sum has been replaced with the option priority="invert"'
         if self.options.ambiguity not in _VALID_AMBIGUITY_OPTIONS:
-            raise ValueError("invalid ambiguity option: %r. Must be one of %r" % (self.options.ambiguity, _VALID_AMBIGUITY_OPTIONS))
+            raise ValueError(
+                "invalid ambiguity option: %r. Must be one of %r"
+                % (self.options.ambiguity, _VALID_AMBIGUITY_OPTIONS)
+            )

         # Parse the grammar file and compose the grammars
-        self.grammar = load_grammar(grammar, self.source_path, self.options.import_paths, self.options.keep_all_tokens)
+        self.grammar = load_grammar(
+            grammar,
+            self.source_path,
+            self.options.import_paths,
+            self.options.keep_all_tokens,
+        )

         if self.options.postlex is not None:
             terminals_to_keep = set(self.options.postlex.always_accept)
         else:
             terminals_to_keep = set()

         # Compile the EBNF grammar into BNF
-        self.terminals, self.rules, self.ignore_tokens = self.grammar.compile(self.options.start, terminals_to_keep)
+        self.terminals, self.rules, self.ignore_tokens = self.grammar.compile(
+            self.options.start, terminals_to_keep
+        )

         if self.options.edit_terminals:
             for t in self.terminals:
                 self.options.edit_terminals(t)

         self._terminals_dict = {t.name: t for t in self.terminals}

         # If the user asked to invert the priorities, negate them all here.
         # This replaces the old 'resolve__antiscore_sum' option.
-        if self.options.priority == 'invert':
+        if self.options.priority == "invert":
             for rule in self.rules:
                 if rule.options.priority is not None:
                     rule.options.priority = -rule.options.priority
         # Else, if the user asked to disable priorities, strip them from the
         # rules. This allows the Earley parsers to skip an extra forest walk
@@ -327,48 +378,60 @@
             for rule in self.rules:
                 if rule.options.priority is not None:
                     rule.options.priority = None

         # TODO Deprecate lexer_callbacks?
-        lexer_callbacks = (_get_lexer_callbacks(self.options.transformer, self.terminals)
-                           if self.options.transformer
-                           else {})
+        lexer_callbacks = (
+            _get_lexer_callbacks(self.options.transformer, self.terminals)
+            if self.options.transformer
+            else {}
+        )
         lexer_callbacks.update(self.options.lexer_callbacks)

-        self.lexer_conf = LexerConf(self.terminals, re_module, self.ignore_tokens, self.options.postlex, lexer_callbacks, self.options.g_regex_flags, use_bytes=self.options.use_bytes)
+        self.lexer_conf = LexerConf(
+            self.terminals,
+            re_module,
+            self.ignore_tokens,
+            self.options.postlex,
+            lexer_callbacks,
+            self.options.g_regex_flags,
+            use_bytes=self.options.use_bytes,
+        )

         if self.options.parser:
             self.parser = self._build_parser()
         elif lexer:
             self.lexer = self._build_lexer()

         if cache_fn:
-            logger.debug('Saving grammar to cache: %s', cache_fn)
-            with FS.open(cache_fn, 'wb') as f:
+            logger.debug("Saving grammar to cache: %s", cache_fn)
+            with FS.open(cache_fn, "wb") as f:
                 self.save(f)

     if __doc__:
         __doc__ += "\n\n" + LarkOptions.OPTIONS_DOC

-    __serialize_fields__ = 'parser', 'rules', 'options'
+    __serialize_fields__ = "parser", "rules", "options"

     def _build_lexer(self):
         return TraditionalLexer(self.lexer_conf)

     def _prepare_callbacks(self):
         self.parser_class = get_frontend(self.options.parser, self.options.lexer)
         self._callbacks = None
         # we don't need these callbacks if we aren't building a tree
-        if self.options.ambiguity != 'forest':
+        if self.options.ambiguity != "forest":
             self._parse_tree_builder = ParseTreeBuilder(
-                    self.rules,
-                    self.options.tree_class or Tree,
-                    self.options.propagate_positions,
-                    self.options.parser != 'lalr' and self.options.ambiguity == 'explicit',
-                    self.options.maybe_placeholders
-                )
-            self._callbacks = self._parse_tree_builder.create_callback(self.options.transformer)
+                self.rules,
+                self.options.tree_class or Tree,
+                self.options.propagate_positions,
+                self.options.parser != "lalr" and self.options.ambiguity == "explicit",
+                self.options.maybe_placeholders,
+            )
+            self._callbacks = self._parse_tree_builder.create_callback(
+                self.options.transformer
+            )

     def _build_parser(self):
         self._prepare_callbacks()
         parser_conf = ParserConf(self.rules, self._callbacks, self.options.start)
         return self.parser_class(self.lexer_conf, parser_conf, options=self.options)
@@ -377,11 +440,11 @@
         """Saves the instance into the given file object

         Useful for caching and multiprocessing.
         """
         data, m = self.memo_serialize([TerminalDef, Rule])
-        pickle.dump({'data': data, 'memo': m}, f, protocol=pickle.HIGHEST_PROTOCOL)
+        pickle.dump({"data": data, "memo": m}, f, protocol=pickle.HIGHEST_PROTOCOL)

     @classmethod
     def load(cls, f):
         """Loads an instance from the given file object

@@ -393,26 +456,31 @@
     def _load(self, f, **kwargs):
         if isinstance(f, dict):
             d = f
         else:
             d = pickle.load(f)
-        memo = d['memo']
-        data = d['data']
+        memo = d["memo"]
+        data = d["data"]

         assert memo
-        memo = SerializeMemoizer.deserialize(memo, {'Rule': Rule, 'TerminalDef': TerminalDef}, {})
-        options = dict(data['options'])
+        memo = SerializeMemoizer.deserialize(
+            memo, {"Rule": Rule, "TerminalDef": TerminalDef}, {}
+        )
+        options = dict(data["options"])
         if (set(kwargs) - _LOAD_ALLOWED_OPTIONS) & set(LarkOptions._defaults):
-            raise ValueError("Some options are not allowed when loading a Parser: {}"
-                             .format(set(kwargs) - _LOAD_ALLOWED_OPTIONS))
+            raise ValueError(
+                "Some options are not allowed when loading a Parser: {}".format(
+                    set(kwargs) - _LOAD_ALLOWED_OPTIONS
+                )
+            )
         options.update(kwargs)
         self.options = LarkOptions.deserialize(options, memo)
-        self.rules = [Rule.deserialize(r, memo) for r in data['rules']]
-        self.source_path = '<deserialized>'
+        self.rules = [Rule.deserialize(r, memo) for r in data["rules"]]
+        self.source_path = "<deserialized>"
         self._prepare_callbacks()
         self.parser = self.parser_class.deserialize(
-            data['parser'],
+            data["parser"],
             memo,
             self._callbacks,
             self.options,  # Not all, but multiple attributes are used
         )
         self.terminals = self.parser.lexer_conf.tokens
@@ -420,11 +488,11 @@
         return self

     @classmethod
     def _load_from_dict(cls, data, memo, **kwargs):
         inst = cls.__new__(cls)
-        return inst._load({'data': data, 'memo': memo}, **kwargs)
+        return inst._load({"data": data, "memo": memo}, **kwargs)

     @classmethod
     def open(cls, grammar_filename, rel_to=None, **options):
         """Create an instance of Lark with the grammar given by its filename

@@ -437,11 +505,11 @@

         """
         if rel_to:
             basepath = os.path.dirname(rel_to)
             grammar_filename = os.path.join(basepath, grammar_filename)
-        with open(grammar_filename, encoding='utf8') as f:
+        with open(grammar_filename, encoding="utf8") as f:
             return cls(f, **options)

     @classmethod
     def open_from_package(cls, package, grammar_path, search_paths=("",), **options):
         """Create an instance of Lark with the grammar loaded from within the package `package`.
@@ -453,22 +521,25 @@

             Lark.open_from_package(__name__, "example.lark", ("grammars",), parser=...)
         """
         package = FromPackageLoader(package, search_paths)
         full_path, text = package(None, grammar_path)
-        options.setdefault('source_path', full_path)
-        options.setdefault('import_paths', [])
-        options['import_paths'].append(package)
+        options.setdefault("source_path", full_path)
+        options.setdefault("import_paths", [])
+        options["import_paths"].append(package)
         return cls(text, **options)

     def __repr__(self):
-        return 'Lark(open(%r), parser=%r, lexer=%r, ...)' % (self.source_path, self.options.parser, self.options.lexer)
-
+        return "Lark(open(%r), parser=%r, lexer=%r, ...)" % (
+            self.source_path,
+            self.options.parser,
+            self.options.lexer,
+        )

     def lex(self, text):
         "Only lex (and postlex) the text, without parsing it. Only relevant when lexer='standard'"
-        if not hasattr(self, 'lexer'):
+        if not hasattr(self, "lexer"):
             self.lexer = self._build_lexer()
         stream = self.lexer.lex(text)
         if self.options.postlex:
             return self.options.postlex.process(stream)
         return stream
@@ -507,34 +578,44 @@
                     raise e

                 if isinstance(e, UnexpectedCharacters):
                     # If user didn't change the character position, then we should
                     if p == s.line_ctr.char_pos:
-                        s.line_ctr.feed(s.text[p:p+1])
+                        s.line_ctr.feed(s.text[p : p + 1])

                 try:
                     return e.puppet.resume_parse()
                 except UnexpectedToken as e2:
-                    if isinstance(e, UnexpectedToken) and e.token.type == e2.token.type == '$END' and e.puppet == e2.puppet:
+                    if (
+                        isinstance(e, UnexpectedToken)
+                        and e.token.type == e2.token.type == "$END"
+                        and e.puppet == e2.puppet
+                    ):
                         # Prevent infinite loop
                         raise e2
                     e = e2
                 except UnexpectedCharacters as e2:
                     e = e2

     @property
     def source(self):
-        warn("Lark.source attribute has been renamed to Lark.source_path", DeprecationWarning)
+        warn(
+            "Lark.source attribute has been renamed to Lark.source_path",
+            DeprecationWarning,
+        )
         return self.source_path

     @source.setter
     def source(self, value):
         self.source_path = value

     @property
     def grammar_source(self):
-        warn("Lark.grammar_source attribute has been renamed to Lark.source_grammar", DeprecationWarning)
+        warn(
+            "Lark.grammar_source attribute has been renamed to Lark.source_grammar",
+            DeprecationWarning,
+        )
         return self.source_grammar

     @grammar_source.setter
     def grammar_source(self, value):
         self.source_grammar = value
--- first pass
+++ second pass
@@ -306,16 +306,20 @@
             elif self.options.parser == "cyk":
                 self.options.lexer = "standard"
             else:
                 assert False, self.options.parser
         lexer = self.options.lexer
-        assert lexer in (
-            "standard",
-            "contextual",
-            "dynamic",
-            "dynamic_complete",
-        ) or issubclass(lexer, Lexer)
+        assert (
+            lexer
+            in (
+                "standard",
+                "contextual",
+                "dynamic",
+                "dynamic_complete",
+            )
+            or issubclass(lexer, Lexer)
+        )

         if self.options.ambiguity == "auto":
             if self.options.parser == "earley":
                 self.options.ambiguity = "resolve"
         else:
ThatXliner commented 3 years ago
Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -59,13 +59,13 @@
         # Make sure "f" works too
         assert _pipe_command(
             self.ls, R' python -m ret "(\w+)\..{4}" f -g 1'
         ) == "\n".join(("poetry", "pyproject"))
         assert _pipe_command(
-            self.ls, R' python -m ret "(?P<some_long_group_name>\w+)\..{4}" f -g some_long_group_name'
+            self.ls,
+            R' python -m ret "(?P<some_long_group_name>\w+)\..{4}" f -g some_long_group_name',
         ) == "\n".join(("poetry", "pyproject"))
-

     def test_search(self):
         assert _pipe_command(
             self.ls, R'python -m ret "LICENSE" search'
         ) == _pipe_command(self.ls, _grep_on_all("LICENSE"))
--- first pass
+++ second pass
@@ -58,14 +58,17 @@

         # Make sure "f" works too
         assert _pipe_command(
             self.ls, R' python -m ret "(\w+)\..{4}" f -g 1'
         ) == "\n".join(("poetry", "pyproject"))
-        assert _pipe_command(
-            self.ls,
-            R' python -m ret "(?P<some_long_group_name>\w+)\..{4}" f -g some_long_group_name',
-        ) == "\n".join(("poetry", "pyproject"))
+        assert (
+            _pipe_command(
+                self.ls,
+                R' python -m ret "(?P<some_long_group_name>\w+)\..{4}" f -g some_long_group_name',
+            )
+            == "\n".join(("poetry", "pyproject"))
+        )

     def test_search(self):
         assert _pipe_command(
             self.ls, R'python -m ret "LICENSE" search'
         ) == _pipe_command(self.ls, _grep_on_all("LICENSE"))
xylix commented 3 years ago

Not a problem for our repo, since this was caught by me missing a folder in our black-ignore. But if this is helpful for debugging this problem then that'd be great. The diff is quite large at around 400 lines so I put it in a gist. Also run black with --verbose and put it below.

diff https://gist.github.com/xylix/9b282167786bc7ba9447ab11db9a6c5b

stacktrace

(venv)  ~/C/robotframework-browser (install-browsers-in-site-packages)> black -v /Users/kerkko/Code/robotframework-browser/Browser/wrapper/node_modules/playwright/.local-browsers/webkit-1383/JavaScriptCore.framework/Versions/A/PrivateHeaders/generate_objc_backend_dispatcher_implementation.py
Using configuration from /Users/kerkko/Code/robotframework-browser/Browser/pyproject.toml.
Traceback (most recent call last):
  File "/Users/kerkko/Code/robotframework-browser/venv/lib/python3.9/site-packages/black/__init__.py", line 670, in reformat_one
    if changed is not Changed.CACHED and format_file_in_place(
  File "/Users/kerkko/Code/robotframework-browser/venv/lib/python3.9/site-packages/black/__init__.py", line 813, in format_file_in_place
    dst_contents = format_file_contents(src_contents, fast=fast, mode=mode)
  File "/Users/kerkko/Code/robotframework-browser/venv/lib/python3.9/site-packages/black/__init__.py", line 940, in format_file_contents
    assert_stable(src_contents, dst_contents, mode=mode)
  File "/Users/kerkko/Code/robotframework-browser/venv/lib/python3.9/site-packages/black/__init__.py", line 6170, in assert_stable
    raise AssertionError(
AssertionError: INTERNAL ERROR: Black produced different code on the second pass of the formatter.  Please report a bug on https://github.com/psf/black/issues.  This diff might be helpful: /var/folders/xz/6y6sty192sbg2m3bb623z6hw0000gn/T/blk_d46pcptu.log
error: cannot format /Users/kerkko/Code/robotframework-browser/Browser/wrapper/node_modules/playwright/.local-browsers/webkit-1383/JavaScriptCore.framework/Versions/A/PrivateHeaders/generate_objc_backend_dispatcher_implementation.py: INTERNAL ERROR: Black produced different code on the second pass of the formatter.  Please report a bug on https://github.com/psf/black/issues.  This diff might be helpful: /var/folders/xz/6y6sty192sbg2m3bb623z6hw0000gn/T/blk_d46pcptu.log
Oh no! 💥 💔 💥
1 file failed to reformat
qiaocco commented 3 years ago
Mode(target_versions={<TargetVersion.PY36: 6>}, line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -38,27 +38,30 @@
 TableReference = namedtuple(
     "TableReference", ["schema", "name", "alias", "is_function"]
 )
 TableReference.ref = property(
     lambda self: self.alias
-                 or (
-                     self.name
-                     if self.name.islower() or self.name[0] == '"'
-                     else '"' + self.name + '"'
-                 )
+    or (
+        self.name
+        if self.name.islower() or self.name[0] == '"'
+        else '"' + self.name + '"'
+    )
 )

 # This code is borrowed from sqlparse example script.
 # <url>
 def is_subselect(parsed):
     if not parsed.is_group:
         return False
     for item in parsed.tokens:
-        if (
-                item.ttype is DML
-                and item.value.upper() in ("SELECT", "INSERT", "UPDATE", "CREATE", "DELETE")
+        if item.ttype is DML and item.value.upper() in (
+            "SELECT",
+            "INSERT",
+            "UPDATE",
+            "CREATE",
+            "DELETE",
         ):
             return True
     return False

@@ -81,22 +84,27 @@
             # StopIteration. So we need to ignore the keyword if the keyword
             # FROM.
             # Also 'SELECT * FROM abc JOIN def' will trigger this elif
             # condition. So we need to ignore the keyword JOIN and its variants
             # INNER JOIN, FULL OUTER JOIN, etc.
-            elif item.ttype is Keyword and (not item.value.upper() == "FROM") and (
-                    not item.value.upper().endswith("JOIN")
+            elif (
+                item.ttype is Keyword
+                and (not item.value.upper() == "FROM")
+                and (not item.value.upper().endswith("JOIN"))
             ):
                 tbl_prefix_seen = False
             else:
                 yield item
         elif item.ttype is Keyword or item.ttype is Keyword.DML:
             item_val = item.value.upper()
-            if (
-                    item_val in ("COPY", "FROM", "INTO", "UPDATE", "TABLE")
-                    or item_val.endswith("JOIN")
-            ):
+            if item_val in (
+                "COPY",
+                "FROM",
+                "INTO",
+                "UPDATE",
+                "TABLE",
+            ) or item_val.endswith("JOIN"):
                 tbl_prefix_seen = True
         # 'SELECT a, FROM abc' will detect FROM as part of the column list.
         # So this check here is necessary.
         elif isinstance(item, IdentifierList):
             for identifier in item.get_identifiers():
@@ -138,12 +146,12 @@
                 # Sometimes Keywords (such as FROM ) are classified as
                 # identifiers which don't have the get_real_name() method.
                 try:
                     schema_name = identifier.get_parent_name()
                     real_name = identifier.get_real_name()
-                    is_function = (
-                            allow_functions and _identifier_is_function(identifier)
+                    is_function = allow_functions and _identifier_is_function(
+                        identifier
                     )
                 except AttributeError:
                     continue
                 if real_name:
                     yield TableReference(
@@ -212,11 +220,11 @@
 #                     print(id.get_real_name())
 #                     columns.append(id)
 #                     print(dir(id))
 #                     print('name:%s, parent_name:%s real_name:%s' %
 #                           (columns[0].get_name(), columns[0].get_parent_name(), columns[0].get_real_name()))
-if __name__ == '__main__':
+if __name__ == "__main__":
     # parse()

     sql = """
 select * from devops.ApplyTest_testlist join devops.ApplyTest_testdeploy ATt on ApplyTest_testlist.id = ATt.testlist_id;    """
     res = extract_tables(sql=sql)
--- first pass
+++ second pass
@@ -94,17 +94,21 @@
                 tbl_prefix_seen = False
             else:
                 yield item
         elif item.ttype is Keyword or item.ttype is Keyword.DML:
             item_val = item.value.upper()
-            if item_val in (
-                "COPY",
-                "FROM",
-                "INTO",
-                "UPDATE",
-                "TABLE",
-            ) or item_val.endswith("JOIN"):
+            if (
+                item_val
+                in (
+                    "COPY",
+                    "FROM",
+                    "INTO",
+                    "UPDATE",
+                    "TABLE",
+                )
+                or item_val.endswith("JOIN")
+            ):
                 tbl_prefix_seen = True
         # 'SELECT a, FROM abc' will detect FROM as part of the column list.
         # So this check here is necessary.
         elif isinstance(item, IdentifierList):
             for identifier in item.get_identifiers():
dadyarri commented 3 years ago

Non-related parts removed; Black 20.08b1 Python 3.8.5 Workaround with --fast works.

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -34,17 +34,20 @@
-    if donate := FinancialDonate.get(
-        category=category_id,
-        student=student_id,
-    ) is not None:
+    if (
+        donate := FinancialDonate.get(
+            category=category_id,
+            student=student_id,
+        )
+        is not None
+    ):
         with db_session:
-            donate.summ = donate.summ + summ,
-            donate.update_date = datetime.now(),
+            donate.summ = (donate.summ + summ,)
+            donate.update_date = (datetime.now(),)
         return donate
     with db_session:
         return FinancialDonate(
             category_id=category_id,
             student_id=student_id,
@@ -116,11 +119,13 @@
-def get_or_create_finances_category(group_id: int, name: str, summ: int) -> FinancialCategory:
+def get_or_create_finances_category(
+    group_id: int, name: str, summ: int
+) -> FinancialCategory:
--- first pass
+++ second pass
@@ -40,11 +40,11 @@
         donate := FinancialDonate.get(
             category=category_id,
             student=student_id,
         )
         is not None
-    ):
+    ) :
         with db_session:
             donate.summ = (donate.summ + summ,)
             donate.update_date = (datetime.now(),)
         return donate
     with db_session:
ThatXliner commented 3 years ago

Black basically missed things. I think the style may be inconsistent... I prefer no whitespace before the colon but eh, it's black

ichard26 commented 3 years ago

@ThatXliner, it's a bug. One that is already fixed in the development branch by commit https://github.com/psf/black/commit/1d2d7264ec7c448744b771910cc972da03b1cb80, now it's just waiting to be included in the next release.

lainproliant commented 3 years ago

Attaching a log I recently experienced, running black --fast <file> twice works to resolve the issue.

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -321,12 +321,12 @@
     @property
     def dirty(self):
         now = datetime.now()
         # type: ignore
         return Recipe.dirty.fget(self) or self.age > max(
-            now - datetime.fromtimestamp(req.stat().st_mtime)
-            for req in self.requires)
+            now - datetime.fromtimestamp(req.stat().st_mtime) for req in self.requires
+        )

     @property
     def age(self) -> timedelta:
         if not self.output.exists():
             return timedelta.max
@@ -512,21 +512,33 @@

     def target(self, f):
         @MethodAttributes.wraps(f)
         async def wrapper(*args, **kwargs):
             result = await async_wrap(f, *args, **kwargs)
-            assert result is not None, "Target definition for '%s' didn't return a value." % f.__name__
+            assert result is not None, (
+                "Target definition for '%s' didn't return a value." % f.__name__
+            )
             if is_iterable(result):
                 results = list(result)
-                assert all(
-                    isinstance(obj, Recipe) for obj in results
-                ), ("Target definition for '%s' returned an iterable containing non-Recipe values (e.g. '%s')." % (
-                    f.__name__, next(type(obj).__qualname__ for obj in result if not isinstance(obj, Recipe))))
+                assert all(isinstance(obj, Recipe) for obj in results), (
+                    "Target definition for '%s' returned an iterable containing non-Recipe values (e.g. '%s')."
+                    % (
+                        f.__name__,
+                        next(
+                            type(obj).__qualname__
+                            for obj in result
+                            if not isinstance(obj, Recipe)
+                        ),
+                    )
+                )
                 result = Recipe(f.__name__, results, isinstance(result, tuple))
             assert isinstance(
                 result, Recipe
-            ), "Target definition for '%s' returned a non-Recipe value ('%s')." % (f.__name__, type(result).__qualname__)
+            ), "Target definition for '%s' returned a non-Recipe value ('%s')." % (
+                f.__name__,
+                type(result).__qualname__,
+            )
             result.origin = f.__name__
             return result

         attrs = MethodAttributes.for_method(wrapper, True, True)
         attrs.put(TARGET_ATTR)
--- first pass
+++ second pass
@@ -517,20 +517,19 @@
             assert result is not None, (
                 "Target definition for '%s' didn't return a value." % f.__name__
             )
             if is_iterable(result):
                 results = list(result)
-                assert all(isinstance(obj, Recipe) for obj in results), (
-                    "Target definition for '%s' returned an iterable containing non-Recipe values (e.g. '%s')."
-                    % (
-                        f.__name__,
-                        next(
-                            type(obj).__qualname__
-                            for obj in result
-                            if not isinstance(obj, Recipe)
-                        ),
-                    )
+                assert all(
+                    isinstance(obj, Recipe) for obj in results
+                ), "Target definition for '%s' returned an iterable containing non-Recipe values (e.g. '%s')." % (
+                    f.__name__,
+                    next(
+                        type(obj).__qualname__
+                        for obj in result
+                        if not isinstance(obj, Recipe)
+                    ),
                 )
                 result = Recipe(f.__name__, results, isinstance(result, tuple))
             assert isinstance(
                 result, Recipe
             ), "Target definition for '%s' returned a non-Recipe value ('%s')." % (
ThatXliner commented 3 years ago

Wait, the --fast option just skips the determistic checking, though. Right?

lainproliant commented 3 years ago

Correct, but after the second pass it becomes stable and can be linted without --fast.

On Sat, Dec 12, 2020, 9:21 AM ThatXliner notifications@github.com wrote:

Wait, the --fast option just skips the determistic checking, though. Right?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/psf/black/issues/1629#issuecomment-743786751, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEPRJ7VQ63I73RNBEQYS53SUORCPANCNFSM4QL5QZSQ .

ichard26 commented 3 years ago

Passing --fast to Black isn't fully safe as Black can produce invalid code. --fast disables the unstable formatting check, the AST equivalence check, and the valid python code check. Black can accidentally destroy a valid Python program; here's an example with --fast.

lainproliant commented 3 years ago

This is true, it is certainly not a fix. But in my case, for this one file, it produced semantically identical code after "stabilizing", and let me move on with using Black normally.

On Sat, Dec 12, 2020, 10:54 AM Richard Si notifications@github.com wrote:

Passing --fast to Black isn't fully safe as Black can produce invalid code. --fast disables the unstable formatting check, the AST equivalence check, and the valid python code check. Black can accidentally destroy a valid Python program; here's an example with --fast https://black.now.sh/?version=stable&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4ACdAHBdAD2IimZxl1N_Wk2dwftOLUrsSjKoBlrvNB7l7P-6rF_SkDWmZI2C0c3VYgLdOTVDVENmN-rJDtUcvMGwCmSaHlEjw7vRCCi9s7cWv0jxpNf7wW495JR8Z-MnHa5iZsy3U0t4cVTyTK20DdL0z1kumG8A2kTgmCMfki8AAYwBngEAAG7VGsuxxGf7AgAAAAAEWVo= .

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/psf/black/issues/1629#issuecomment-743799782, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEPRJZD7FGWH4XGRMUDUF3SUO35PANCNFSM4QL5QZSQ .

graingert commented 3 years ago
INTERNAL ERROR: Black produced different code on the second pass of the formatter.  Please report a bug on https://github.com/psf/black/issues.  This diff might be helpful: /tmp/blk_pk9o3j1v.log

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)

--- source
+++ first pass
@@ -1,6 +1,8 @@
 def fn():
     if True:
         if True:
-            if fn(a.form['password'].encode('utf-8'), signin_user['password'].encode('utf-8')) == \
-                    c['d'].e('f'):
+            if fn(
+                a.form["password"].encode("utf-8"),
+                signin_user["password"].encode("utf-8"),
+            ) == c["d"].e("f"):
                 pass
--- first pass
+++ second pass
@@ -1,8 +1,11 @@
 def fn():
     if True:
         if True:
-            if fn(
-                a.form["password"].encode("utf-8"),
-                signin_user["password"].encode("utf-8"),
-            ) == c["d"].e("f"):
+            if (
+                fn(
+                    a.form["password"].encode("utf-8"),
+                    signin_user["password"].encode("utf-8"),
+                )
+                == c["d"].e("f")
+            ):
                 pass
atodorov commented 3 years ago

black==20.8b1

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -9,114 +9,130 @@
 from simple_history.models import HistoricalRecords

 def diff_objects(old_instance, new_instance, fields):
     """
-        Diff two objects by examining the given fields and
-        return a string.
+    Diff two objects by examining the given fields and
+    return a string.
     """
     full_diff = []

     for field in fields:
         field_diff = []
         old_value = getattr(old_instance, field.attname)
         new_value = getattr(new_instance, field.attname)
-        for line in difflib.unified_diff(str(old_value).split('\n'),
-                                         str(new_value).split('\n'),
-                                         fromfile=field.attname,
-                                         tofile=field.attname,
-                                         lineterm=""):
+        for line in difflib.unified_diff(
+            str(old_value).split("\n"),
+            str(new_value).split("\n"),
+            fromfile=field.attname,
+            tofile=field.attname,
+            lineterm="",
+        ):
             field_diff.append(line)
         full_diff.extend(field_diff)

     return "\n".join(full_diff)

 def history_email_for(instance, title):
     """
-        Generate the subject and email body that is sent via
-        email notifications post update!
+    Generate the subject and email body that is sent via
+    email notifications post update!
     """
     history = instance.history.latest()

     subject = _("UPDATE: %(model_name)s #%(pk)d - %(title)s") % {
-        'model_name': instance.__class__.__name__,
-        'pk': instance.pk,
-        'title': title
+        "model_name": instance.__class__.__name__,
+        "pk": instance.pk,
+        "title": title,
     }

-    body = _("""Updated on %(history_date)s
+    body = (
+        _(
+            """Updated on %(history_date)s
 Updated by %(username)s

 %(diff)s

 For more information:
-%(instance_url)s""") % {'history_date': history.history_date.strftime('%c'),
-                        'username': getattr(history.history_user, 'username', ''),
-                        'diff': history.history_change_reason,
-                        'instance_url': instance.get_full_url()}
+%(instance_url)s"""
+        )
+        % {
+            "history_date": history.history_date.strftime("%c"),
+            "username": getattr(history.history_user, "username", ""),
+            "diff": history.history_change_reason,
+            "instance_url": instance.get_full_url(),
+        }
+    )
     return subject, body

 class KiwiHistoricalRecords(HistoricalRecords):
     """
-        This class will keep track of what fields were changed
-        inside of the ``history_change_reason`` field. This gives us
-        a crude changelog until upstream introduces their new interface.
+    This class will keep track of what fields were changed
+    inside of the ``history_change_reason`` field. This gives us
+    a crude changelog until upstream introduces their new interface.
     """
+
     def pre_save(self, instance, **kwargs):
         """
-            Signal handlers don't have access to the previous version of
-            an object so we have to load it from the database!
+        Signal handlers don't have access to the previous version of
+        an object so we have to load it from the database!
         """
-        if kwargs.get('raw', False):
+        if kwargs.get("raw", False):
             return

-        if instance.pk and hasattr(instance, 'history'):
-            instance.previous = instance.__class__.objects.filter(pk=instance.pk).first()
+        if instance.pk and hasattr(instance, "history"):
+            instance.previous = instance.__class__.objects.filter(
+                pk=instance.pk
+            ).first()

     def post_save(self, instance, created, using=None, **kwargs):
         """
-            Calculate the changelog and call the inherited method to
-            write the data into the database.
+        Calculate the changelog and call the inherited method to
+        write the data into the database.
         """
-        if kwargs.get('raw', False):
+        if kwargs.get("raw", False):
             return

-        if hasattr(instance, 'previous') and instance.previous:
+        if hasattr(instance, "previous") and instance.previous:
             # note: simple_history.utils.update_change_reason() performs an extra
             # DB query so it is better to use the private field instead!
             # In older simple_history version this field wasn't private but was renamed
             # in 2.10.0 hence the pylint disable!
             instance._change_reason = diff_objects(  # pylint: disable=protected-access
-                instance.previous, instance, self.fields_included(instance))
+                instance.previous, instance, self.fields_included(instance)
+            )
         super().post_save(instance, created, using, **kwargs)

     def finalize(self, sender, **kwargs):
         """
-            Connect the pre_save signal handler after calling the inherited method.
+        Connect the pre_save signal handler after calling the inherited method.
         """
         super().finalize(sender, **kwargs)
         signals.pre_save.connect(self.pre_save, sender=sender, weak=False)

 class ReadOnlyHistoryAdmin(SimpleHistoryAdmin):
     """
-        Custom history admin which shows all fields
-        as read-only.
+    Custom history admin which shows all fields
+    as read-only.
     """
-    history_list_display = ['Diff']
+
+    history_list_display = ["Diff"]

     def Diff(self, obj):  # pylint: disable=invalid-name
-        return safe('<pre>%s</pre>' % obj.history_change_reason)
+        return safe("<pre>%s</pre>" % obj.history_change_reason)

     def get_readonly_fields(self, request, obj=None):
         # make all fields readonly
-        readonly_fields = list(set(
-            [field.name for field in self.opts.local_fields] +
-            [field.name for field in self.opts.local_many_to_many]
-        ))
+        readonly_fields = list(
+            set(
+                [field.name for field in self.opts.local_fields]
+                + [field.name for field in self.opts.local_many_to_many]
+            )
+        )
         return readonly_fields

     def response_change(self, request, obj):
         super().response_change(request, obj)
         return HttpResponseRedirect(obj.get_absolute_url())
--- first pass
+++ second pass
@@ -44,27 +44,24 @@
         "model_name": instance.__class__.__name__,
         "pk": instance.pk,
         "title": title,
     }

-    body = (
-        _(
-            """Updated on %(history_date)s
+    body = _(
+        """Updated on %(history_date)s
 Updated by %(username)s

 %(diff)s

 For more information:
 %(instance_url)s"""
-        )
-        % {
-            "history_date": history.history_date.strftime("%c"),
-            "username": getattr(history.history_user, "username", ""),
-            "diff": history.history_change_reason,
-            "instance_url": instance.get_full_url(),
-        }
-    )
+    ) % {
+        "history_date": history.history_date.strftime("%c"),
+        "username": getattr(history.history_user, "username", ""),
+        "diff": history.history_change_reason,
+        "instance_url": instance.get_full_url(),
+    }
     return subject, body

 class KiwiHistoricalRecords(HistoricalRecords):
     """
feuloren commented 3 years ago

Please find below the generated log when try to format this file


def process_import(file_import, format="csv"):
    # ignore files that are not csv
    if file_import.provider in [Provider.PAYLINE, Provider.ADYEN] and not file_import.path.endswith("csv"):
        return
Mode(target_versions={<TargetVersion.PY36: 6>}, line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,5 +1,7 @@
-
 def process_import(file_import, format="csv"):
     # ignore files that are not csv
-    if file_import.provider in [Provider.PAYLINE, Provider.ADYEN] and not file_import.path.endswith("csv"):
+    if file_import.provider in [
+        Provider.PAYLINE,
+        Provider.ADYEN,
+    ] and not file_import.path.endswith("csv"):
         return
--- first pass
+++ second pass
@@ -1,7 +1,11 @@
 def process_import(file_import, format="csv"):
     # ignore files that are not csv
-    if file_import.provider in [
-        Provider.PAYLINE,
-        Provider.ADYEN,
-    ] and not file_import.path.endswith("csv"):
+    if (
+        file_import.provider
+        in [
+            Provider.PAYLINE,
+            Provider.ADYEN,
+        ]
+        and not file_import.path.endswith("csv")
+    ):
         return
ghost commented 3 years ago

Hello! I used Black in pre-commit hook (rev: 20.8b1) and got: error: cannot format /home/path/to/tests/test_task1.py: INTERNAL ERROR: Black produced different code on the second pass of the formatter. blk_krk51l0g.log

OS: Linux Debian 10. IDE: VS Code v.1.52.0.

samuelstevens commented 3 years ago

Here's my log file.

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -192,26 +192,28 @@

     word_ends = []

     # check if there are any tokens that need to be split into words.
     if _split_precondition(tokens, words):
-        tokens_to_split = set(['),', ').'])
+        tokens_to_split = set(["),", ")."])

         word_i = 0
         token_i = 0
         while word_i < len(words) and token_i < len(tokens):
             if (
                 words[word_i].lower().endswith(tokens[token_i].lower().strip())
                 and tokens[token_i].strip()
             ):
                 word_ends.append(tokens[token_i])
                 word_i += 1
-            elif tokens[token_i] in tokens_to_split and (words[word_i], words[word_i+1]) == tuple(tokens[token_i]):
+            elif tokens[token_i] in tokens_to_split and (
+                words[word_i],
+                words[word_i + 1],
+            ) == tuple(tokens[token_i]):
                 word_ends.append(tokens[token_i])
                 word_ends.append(tokens[token_i])
                 word_i += 1
-

             token_i += 1
     else:
         word_i = 0
         token_i = 0
--- first pass
+++ second pass
@@ -203,14 +203,18 @@
                 words[word_i].lower().endswith(tokens[token_i].lower().strip())
                 and tokens[token_i].strip()
             ):
                 word_ends.append(tokens[token_i])
                 word_i += 1
-            elif tokens[token_i] in tokens_to_split and (
-                words[word_i],
-                words[word_i + 1],
-            ) == tuple(tokens[token_i]):
+            elif (
+                tokens[token_i] in tokens_to_split
+                and (
+                    words[word_i],
+                    words[word_i + 1],
+                )
+                == tuple(tokens[token_i])
+            ):
                 word_ends.append(tokens[token_i])
                 word_ends.append(tokens[token_i])
                 word_i += 1

             token_i += 1
spagh-eddie commented 3 years ago

If you find a case of this, please attach the generated log here so we can investigate.

The --fast workaround is OK for now.

$ black --version
black, version 20.8b1
Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,5 +1,13 @@
 for i in range(registers_count):
-    msg = 'HALF_PAGE = %2g INSTANCE = %2g NAME = %s ADDR = h%0X - %g | RDATA = h%0X  | EDATA = h%0X' % \ 
-          (half_pages_array[i], instances_array[i], name_array[i],
-           address_array[i], address_array[i], rdata_array[i],
-           edata_array[i])
+    msg = ( 
+        "HALF_PAGE = %2g INSTANCE = %2g NAME = %s ADDR = h%0X - %g | RDATA = h%0X  | EDATA = h%0X"
+        % ( 
+            half_pages_array[i],
+            instances_array[i],
+            name_array[i],
+            address_array[i],
+            address_array[i],
+            rdata_array[i],
+            edata_array[i],
+        )   
+    )   
--- first pass
+++ second pass
@@ -1,13 +1,10 @@
 for i in range(registers_count):
-    msg = ( 
-        "HALF_PAGE = %2g INSTANCE = %2g NAME = %s ADDR = h%0X - %g | RDATA = h%0X  | EDATA = h%0X"
-        % ( 
-            half_pages_array[i],
-            instances_array[i],
-            name_array[i],
-            address_array[i],
-            address_array[i],
-            rdata_array[i],
-            edata_array[i],
-        )   
+    msg = "HALF_PAGE = %2g INSTANCE = %2g NAME = %s ADDR = h%0X - %g | RDATA = h%0X  | EDATA = h%0X" % ( 
+        half_pages_array[i],
+        instances_array[i],
+        name_array[i],
+        address_array[i],
+        address_array[i],
+        rdata_array[i],
+        edata_array[i],
     )   
JCJutson commented 3 years ago

I found this error:

Mode(target_versions=set(), line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -3,8 +3,15 @@
 # -*- coding: utf-8 -*-

 class Foo(object):
     def bar(self):
-        x = 'foo ' + \
-            'subscription {} resource_group {} Site {}, foobar_name {}. Error {}'.format(
-                self._subscription, self._resource_group, self._site, self._foobar_name, error)
+        x = (
+            "foo "
+            + "subscription {} resource_group {} Site {}, foobar_name {}. Error {}".format(
+                self._subscription,
+                self._resource_group,
+                self._site,
+                self._foobar_name,
+                error,
+            )
+        )
--- first pass
+++ second pass
@@ -3,15 +3,12 @@
 # -*- coding: utf-8 -*-

 class Foo(object):
     def bar(self):
-        x = (
-            "foo "
-            + "subscription {} resource_group {} Site {}, foobar_name {}. Error {}".format(
-                self._subscription,
-                self._resource_group,
-                self._site,
-                self._foobar_name,
-                error,
-            )
+        x = "foo " + "subscription {} resource_group {} Site {}, foobar_name {}. Error {}".format(
+            self._subscription,
+            self._resource_group,
+            self._site,
+            self._foobar_name,
+            error,
         )

From the code:

#!/usr/bin/env python3

# -*- coding: utf-8 -*-

class Foo(object):
    def bar(self):
        x = 'foo ' + \
            'subscription {} resource_group {} Site {}, foobar_name {}. Error {}'.format(
                self._subscription, self._resource_group, self._site, self._foobar_name, error)            
christophebiocca commented 3 years ago

Case 1:

Mode(target_versions={<TargetVersion.PY37: 7>, <TargetVersion.PY36: 6>, <TargetVersion.PY38: 8>}, line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,13 +1,8 @@
 class A:
     def a():
-        return (
-            A(aaaaaa) | Aaaaaaaaaaaa(
-                lambda aaaaaaa: aaaaaaaaa.Aaaaaa(
-                    aaaaaa=aaaaaa.Aaaaaaaaaaaaaaaaaaaaaa.aaaaaaaa,
-                    aaaaa=aaaaaaaaa.Aaaaaa(
-                        a,
-                        a
-                    )
-                )
-            ).a(a)
-        )
+        return A(aaaaaa) | Aaaaaaaaaaaa(
+            lambda aaaaaaa: aaaaaaaaa.Aaaaaa(
+                aaaaaa=aaaaaa.Aaaaaaaaaaaaaaaaaaaaaa.aaaaaaaa,
+                aaaaa=aaaaaaaaa.Aaaaaa(a, a),
+            )
+        ).a(a)
--- first pass
+++ second pass
@@ -1,8 +1,11 @@
 class A:
     def a():
-        return A(aaaaaa) | Aaaaaaaaaaaa(
-            lambda aaaaaaa: aaaaaaaaa.Aaaaaa(
-                aaaaaa=aaaaaa.Aaaaaaaaaaaaaaaaaaaaaa.aaaaaaaa,
-                aaaaa=aaaaaaaaa.Aaaaaa(a, a),
-            )
-        ).a(a)
+        return (
+            A(aaaaaa)
+            | Aaaaaaaaaaaa(
+                lambda aaaaaaa: aaaaaaaaa.Aaaaaa(
+                    aaaaaa=aaaaaa.Aaaaaaaaaaaaaaaaaaaaaa.aaaaaaaa,
+                    aaaaa=aaaaaaaaa.Aaaaaa(a, a),
+                )
+            ).a(a)
+        )

Case 2:

Mode(target_versions={<TargetVersion.PY37: 7>, <TargetVersion.PY36: 6>, <TargetVersion.PY38: 8>}, line_length=88, string_normalization=True, experimental_string_processing=False, is_pyi=False)
--- source
+++ first pass
@@ -1,4 +1,8 @@
 def a():
     assert a(
-        aaaaaaaaaaaa=aaa, aaaaaaaaaaa=aaa, aaaaaaaaaaaaaa=aaa, aaaaaaaaaaaaa=aaa, aaaaaaaaaaaaaaaaa=aaaa
+        aaaaaaaaaaaa=aaa,
+        aaaaaaaaaaa=aaa,
+        aaaaaaaaaaaaaa=aaa,
+        aaaaaaaaaaaaa=aaa,
+        aaaaaaaaaaaaaaaaa=aaaa,
     ) == (0, 0)
--- first pass
+++ second pass
@@ -1,8 +1,11 @@
 def a():
-    assert a(
-        aaaaaaaaaaaa=aaa,
-        aaaaaaaaaaa=aaa,
-        aaaaaaaaaaaaaa=aaa,
-        aaaaaaaaaaaaa=aaa,
-        aaaaaaaaaaaaaaaaa=aaaa,
-    ) == (0, 0)
+    assert (
+        a(
+            aaaaaaaaaaaa=aaa,
+            aaaaaaaaaaa=aaa,
+            aaaaaaaaaaaaaa=aaa,
+            aaaaaaaaaaaaa=aaa,
+            aaaaaaaaaaaaaaaaa=aaaa,
+        )
+        == (0, 0)
+    )