Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a rule/autofix to sort __slots__ and __match_args__ #9564

Merged
merged 23 commits into from
Jan 22, 2024

Conversation

AlexWaygood
Copy link
Member

@AlexWaygood AlexWaygood commented Jan 17, 2024

Summary

This PR introduces a new rule to sort __slots__ and __match_args__ according to a natural sort, as was requested in #1198 (comment).

The implementation here generalises some of the machinery introduced in 3aae16f so that different kinds of sorts can be applied to lists of string literals. (We use an "isort-style" sort for __all__, but that isn't really appropriate for __slots__ and __match_args__, where nearly all items will be snake_case.) Several sections of code have been moved from sort_dunder_all.rs to a new module, sorting_helpers.rs, which sort_dunder_all.rs and sort_dunder_slots.rs both make use of.

__match_args__ is very similar to __all__, in that it can only be a tuple or a list. __slots__ differs from the other two, however, in that it can be any iterable of strings. If slots is a dictionary, the values are used by the builtin help() function as per-attribute docstrings that show up in the output of help(). (There's no particular use-case for making __slots__ a set, but it's perfectly legal at runtime, so there's no reason for us not to handle it in this rule.)

Currently I've only implemented an autofix for __match_args__ and __slots__ if the definition is on a single line. That's for two reasons:

  1. I wanted to see what people thought of this before going any further!
  2. Multiline __slots__ and __match_args__ definitions are much rarer than multiline __all__ definitions, so there's probably less value in implementing the autofix for those.

Implementing an autofix for multiline __match_args__ wouldn't be too hard: I'd just have to move more code from sort_dunder_all.rs to sorting_helpers.rs. The same is true for __slots__ when it's a tuple, list or set. I don't feel enthused about implementing an autofix for multiline __slots__ definitions where __slots__ is a dict, however. That would require a lot of extra code, and using a dict for __slots__ is pretty rare.

If we're interested in autofixes for multiline __slots__ and __match_args__ definitions, let me know whether you'd prefer it in this PR or a followup!

Test Plan

cargo test / cargo insta review.

I also ran this rule on CPython. The autofix fixed 45 violations:

Diff from the autofix
diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py
index bca2acf1fc..657544aa47 100644
--- a/Lib/_pydatetime.py
+++ b/Lib/_pydatetime.py
@@ -600,7 +600,7 @@ class timedelta:
     # arbitrarily; the exact rationale originally specified in the docstring
     # was "Because I felt like it."
 
-    __slots__ = '_days', '_seconds', '_microseconds', '_hashcode'
+    __slots__ = '_days', '_hashcode', '_microseconds', '_seconds'
 
     def __new__(cls, days=0, seconds=0, microseconds=0,
                 milliseconds=0, minutes=0, hours=0, weeks=0):
@@ -931,7 +931,7 @@ class date:
     Properties (readonly):
     year, month, day
     """
-    __slots__ = '_year', '_month', '_day', '_hashcode'
+    __slots__ = '_day', '_hashcode', '_month', '_year'
 
     def __new__(cls, year, month=None, day=None):
         """Constructor.
@@ -1346,7 +1346,7 @@ class time:
     Properties (readonly):
     hour, minute, second, microsecond, tzinfo, fold
     """
-    __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode', '_fold'
+    __slots__ = '_fold', '_hashcode', '_hour', '_microsecond', '_minute', '_second', '_tzinfo'
 
     def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0):
         """Constructor.
@@ -2328,7 +2328,7 @@ def _isoweek1monday(year):
 
 
 class timezone(tzinfo):
-    __slots__ = '_offset', '_name'
+    __slots__ = '_name', '_offset'
 
     # Sentinel value to disallow None
     _Omitted = object()
diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py
index 2692f2fcba..cd7929407d 100644
--- a/Lib/_pydecimal.py
+++ b/Lib/_pydecimal.py
@@ -523,7 +523,7 @@ def sin(x):
 class Decimal(object):
     """Floating point class for decimal arithmetic."""
 
-    __slots__ = ('_exp','_int','_sign', '_is_special')
+    __slots__ = ('_exp', '_int', '_is_special', '_sign')
     # Generally, the value of the Decimal instance is given by
     #  (-1)**_sign * _int * 10**_exp
     # Special values are signified by _is_special == True
@@ -5626,7 +5626,7 @@ def to_integral_value(self, a):
     to_integral = to_integral_value
 
 class _WorkRep(object):
-    __slots__ = ('sign','int','exp')
+    __slots__ = ('exp', 'int', 'sign')
     # sign: 0 or 1
     # int:  int
     # exp:  None, int, or string
diff --git a/Lib/_threading_local.py b/Lib/_threading_local.py
index b006d76c4e..3acba9e055 100644
--- a/Lib/_threading_local.py
+++ b/Lib/_threading_local.py
@@ -145,7 +145,7 @@
 
 class _localimpl:
     """A class managing thread-local dicts"""
-    __slots__ = 'key', 'dicts', 'localargs', 'locallock', '__weakref__'
+    __slots__ = '__weakref__', 'dicts', 'key', 'localargs', 'locallock'
 
     def __init__(self):
         # The key used in the Thread objects' attribute dicts.
@@ -202,7 +202,7 @@ def _patch(self):
 
 
 class local:
-    __slots__ = '_local__impl', '__dict__'
+    __slots__ = '__dict__', '_local__impl'
 
     def __new__(cls, /, *args, **kw):
         if (args or kw) and (cls.__init__ is object.__init__):
diff --git a/Lib/asyncio/transports.py b/Lib/asyncio/transports.py
index 30fd41d49a..f3e641cb53 100644
--- a/Lib/asyncio/transports.py
+++ b/Lib/asyncio/transports.py
@@ -265,7 +265,7 @@ class _FlowControlMixin(Transport):
     resume_writing() may be called.
     """
 
-    __slots__ = ('_loop', '_protocol_paused', '_high_water', '_low_water')
+    __slots__ = ('_high_water', '_loop', '_low_water', '_protocol_paused')
 
     def __init__(self, extra=None, loop=None):
         super().__init__(extra)
diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py
index 2e527dfd81..62f76b903d 100644
--- a/Lib/collections/__init__.py
+++ b/Lib/collections/__init__.py
@@ -78,7 +78,7 @@ def __reversed__(self):
             yield self._mapping[key]
 
 class _Link(object):
-    __slots__ = 'prev', 'next', 'key', '__weakref__'
+    __slots__ = '__weakref__', 'key', 'next', 'prev'
 
 class OrderedDict(dict):
     'Dictionary that remembers insertion order'
diff --git a/Lib/fractions.py b/Lib/fractions.py
index 389ab386b6..f4f524d66e 100644
--- a/Lib/fractions.py
+++ b/Lib/fractions.py
@@ -197,7 +197,7 @@ class Fraction(numbers.Rational):
 
     """
 
-    __slots__ = ('_numerator', '_denominator')
+    __slots__ = ('_denominator', '_numerator')
 
     # We're immutable, so use __new__ not __init__
     def __new__(cls, numerator=0, denominator=None):
diff --git a/Lib/functools.py b/Lib/functools.py
index 55990e742b..a936d85185 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -279,7 +279,7 @@ class partial:
     and keywords.
     """
 
-    __slots__ = "func", "args", "keywords", "__dict__", "__weakref__"
+    __slots__ = "__dict__", "__weakref__", "args", "func", "keywords"
 
     def __new__(cls, func, /, *args, **keywords):
         if not callable(func):
diff --git a/Lib/inspect.py b/Lib/inspect.py
index f0b72662a9..d27ff1a2fc 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -2751,7 +2751,7 @@ class Parameter:
         `Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`.
     """
 
-    __slots__ = ('_name', '_kind', '_default', '_annotation')
+    __slots__ = ('_annotation', '_default', '_kind', '_name')
 
     POSITIONAL_ONLY         = _POSITIONAL_ONLY
     POSITIONAL_OR_KEYWORD   = _POSITIONAL_OR_KEYWORD
@@ -2906,7 +2906,7 @@ class BoundArguments:
         Dict of keyword arguments values.
     """
 
-    __slots__ = ('arguments', '_signature', '__weakref__')
+    __slots__ = ('__weakref__', '_signature', 'arguments')
 
     def __init__(self, signature, arguments):
         self.arguments = arguments
@@ -3042,7 +3042,7 @@ class Signature:
         to parameters (simulating 'functools.partial' behavior.)
     """
 
-    __slots__ = ('_return_annotation', '_parameters')
+    __slots__ = ('_parameters', '_return_annotation')
 
     _parameter_cls = Parameter
     _bound_arguments_cls = BoundArguments
diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py
index e398cc1383..e7e6e20f79 100644
--- a/Lib/ipaddress.py
+++ b/Lib/ipaddress.py
@@ -1277,7 +1277,7 @@ class IPv4Address(_BaseV4, _BaseAddress):
 
     """Represent and manipulate single IPv4 Addresses."""
 
-    __slots__ = ('_ip', '__weakref__')
+    __slots__ = ('__weakref__', '_ip')
 
     def __init__(self, address):
 
@@ -1891,7 +1891,7 @@ class IPv6Address(_BaseV6, _BaseAddress):
 
     """Represent and manipulate single IPv6 Addresses."""
 
-    __slots__ = ('_ip', '_scope_id', '__weakref__')
+    __slots__ = ('__weakref__', '_ip', '_scope_id')
 
     def __init__(self, address):
         """Instantiate a new IPv6 address object.
diff --git a/Lib/multiprocessing/managers.py b/Lib/multiprocessing/managers.py
index 76b915de74..60d6c622c1 100644
--- a/Lib/multiprocessing/managers.py
+++ b/Lib/multiprocessing/managers.py
@@ -63,7 +63,7 @@ class Token(object):
     '''
     Type to uniquely identify a shared object
     '''
-    __slots__ = ('typeid', 'address', 'id')
+    __slots__ = ('address', 'id', 'typeid')
 
     def __init__(self, typeid, address, id):
         (self.typeid, self.address, self.id) = (typeid, address, id)
diff --git a/Lib/operator.py b/Lib/operator.py
index 30116c1189..2f7371f42a 100644
--- a/Lib/operator.py
+++ b/Lib/operator.py
@@ -274,7 +274,7 @@ class itemgetter:
     After f = itemgetter(2), the call f(r) returns r[2].
     After g = itemgetter(2, 5, 3), the call g(r) returns (r[2], r[5], r[3])
     """
-    __slots__ = ('_items', '_call')
+    __slots__ = ('_call', '_items')
 
     def __init__(self, item, *items):
         if not items:
@@ -306,7 +306,7 @@ class methodcaller:
     After g = methodcaller('name', 'date', foo=1), the call g(r) returns
     r.name('date', foo=1).
     """
-    __slots__ = ('_name', '_args', '_kwargs')
+    __slots__ = ('_args', '_kwargs', '_name')
 
     def __init__(self, name, /, *args, **kwargs):
         self._name = name
diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py
index f14d35bb00..7dbf627702 100644
--- a/Lib/pathlib/__init__.py
+++ b/Lib/pathlib/__init__.py
@@ -45,7 +45,7 @@
 class _PathParents(Sequence):
     """This object provides sequence-like access to the logical ancestors
     of a path.  Don't try to construct it yourself."""
-    __slots__ = ('_path', '_drv', '_root', '_tail')
+    __slots__ = ('_drv', '_path', '_root', '_tail')
 
     def __init__(self, path):
         self._path = path
diff --git a/Lib/socket.py b/Lib/socket.py
index 77986fc2e4..909a881808 100644
--- a/Lib/socket.py
+++ b/Lib/socket.py
@@ -216,7 +216,7 @@ class socket(_socket.socket):
 
     """A subclass of _socket.socket adding the makefile() method."""
 
-    __slots__ = ["__weakref__", "_io_refs", "_closed"]
+    __slots__ = ["__weakref__", "_closed", "_io_refs"]
 
     def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
         # For user code address family and type values are IntEnum members, but
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 8bda17358d..1d84157323 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -150,7 +150,7 @@ def __init__(self, offset=None, name=None, dstoffset=None):
         FixedOffset.__init__(self, offset, name, dstoffset)
 
 class PicklableFixedOffsetWithSlots(PicklableFixedOffset):
-    __slots__ = '_FixedOffset__offset', '_FixedOffset__name', 'spam'
+    __slots__ = '_FixedOffset__name', '_FixedOffset__offset', 'spam'
 
 class _TZInfo(tzinfo):
     def utcoffset(self, datetime_module):
diff --git a/Lib/test/test_binop.py b/Lib/test/test_binop.py
index 299af09c49..19fafe865d 100644
--- a/Lib/test/test_binop.py
+++ b/Lib/test/test_binop.py
@@ -29,7 +29,7 @@ class Rat(object):
 
     """Rational number implemented as a normalized pair of ints."""
 
-    __slots__ = ['_Rat__num', '_Rat__den']
+    __slots__ = ['_Rat__den', '_Rat__num']
 
     def __init__(self, num=0, den=1):
         """Constructor: Rat([num[, den]]).
diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py
index a3804a945f..50f162cb2b 100644
--- a/Lib/test/test_bytes.py
+++ b/Lib/test/test_bytes.py
@@ -2092,7 +2092,7 @@ class ByteArraySubclass(bytearray):
     pass
 
 class ByteArraySubclassWithSlots(bytearray):
-    __slots__ = ('x', 'y', '__dict__')
+    __slots__ = ('__dict__', 'x', 'y')
 
 class BytesSubclass(bytes):
     pass
diff --git a/Lib/test/test_deque.py b/Lib/test/test_deque.py
index ae1dfacd72..1d6199e3f4 100644
--- a/Lib/test/test_deque.py
+++ b/Lib/test/test_deque.py
@@ -782,7 +782,7 @@ class Deque(deque):
     pass
 
 class DequeWithSlots(deque):
-    __slots__ = ('x', 'y', '__dict__')
+    __slots__ = ('__dict__', 'x', 'y')
 
 class DequeWithBadIter(deque):
     def __iter__(self):
diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py
index 1d71dd9e26..6b5ff082f6 100644
--- a/Lib/test/test_gc.py
+++ b/Lib/test/test_gc.py
@@ -1042,7 +1042,7 @@ def test_trash_weakref_clear(self):
         callback = unittest.mock.Mock()
 
         class A:
-            __slots__ = ['a', 'y', 'wz']
+            __slots__ = ['a', 'wz', 'y']
 
         class Z:
             pass
diff --git a/Lib/test/test_patma.py b/Lib/test/test_patma.py
index 298e78ccee..ab5b856991 100644
--- a/Lib/test/test_patma.py
+++ b/Lib/test/test_patma.py
@@ -3303,7 +3303,7 @@ class Class:
 
     def test_match_args_must_be_a_tuple_2(self):
         class Class:
-            __match_args__ = ["spam", "eggs"]
+            __match_args__ = ["eggs", "spam"]
             spam = 0
             eggs = 1
         x = Class()
diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py
index c12c908d2e..cf7d424ea9 100644
--- a/Lib/test/test_property.py
+++ b/Lib/test/test_property.py
@@ -275,7 +275,7 @@ def documented_getter():
     def test_property_with_slots_and_doc_slot_docstring_present(self):
         # https://github.com/python/cpython/issues/98963#issuecomment-1574413319
         class slotted_prop(property):
-            __slots__ = ("foo", "__doc__")
+            __slots__ = ("__doc__", "foo")
 
         p = slotted_prop(doc="what's up")
         self.assertEqual("what's up", p.__doc__)  # new in 3.12: This gets set.
diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py
index d9102eb98a..d19a377b85 100644
--- a/Lib/test/test_set.py
+++ b/Lib/test/test_set.py
@@ -809,7 +809,7 @@ def test_singleton_empty_frozenset(self):
 
 
 class SetSubclassWithSlots(set):
-    __slots__ = ('x', 'y', '__dict__')
+    __slots__ = ('__dict__', 'x', 'y')
 
 class TestSetSubclassWithSlots(unittest.TestCase):
     thetype = SetSubclassWithSlots
@@ -817,7 +817,7 @@ class TestSetSubclassWithSlots(unittest.TestCase):
     test_pickling = TestJointOps.test_pickling
 
 class FrozenSetSubclassWithSlots(frozenset):
-    __slots__ = ('x', 'y', '__dict__')
+    __slots__ = ('__dict__', 'x', 'y')
 
 class TestFrozenSetSubclassWithSlots(TestSetSubclassWithSlots):
     thetype = FrozenSetSubclassWithSlots
diff --git a/Lib/tracemalloc.py b/Lib/tracemalloc.py
index cec99c5970..aca59617cf 100644
--- a/Lib/tracemalloc.py
+++ b/Lib/tracemalloc.py
@@ -32,7 +32,7 @@ class Statistic:
     Statistic difference on memory allocations between two Snapshot instance.
     """
 
-    __slots__ = ('traceback', 'size', 'count')
+    __slots__ = ('count', 'size', 'traceback')
 
     def __init__(self, traceback, size, count):
         self.traceback = traceback
@@ -72,7 +72,7 @@ class StatisticDiff:
     Statistic difference on memory allocations between an old and a new
     Snapshot instance.
     """
-    __slots__ = ('traceback', 'size', 'size_diff', 'count', 'count_diff')
+    __slots__ = ('count', 'count_diff', 'size', 'size_diff', 'traceback')
 
     def __init__(self, traceback, size, size_diff, count, count_diff):
         self.traceback = traceback
diff --git a/Lib/typing.py b/Lib/typing.py
index d278b4effc..85c1b269c2 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -448,7 +448,7 @@ def __iter__(self): raise TypeError()
 # Internal indicator of special typing constructs.
 # See __doc__ instance attribute for specific docs.
 class _SpecialForm(_Final, _NotIterable, _root=True):
-    __slots__ = ('_name', '__doc__', '_getitem')
+    __slots__ = ('__doc__', '_getitem', '_name')
 
     def __init__(self, getitem):
         self._getitem = getitem
diff --git a/Lib/uuid.py b/Lib/uuid.py
index 470bc0d685..4b405c35b9 100644
--- a/Lib/uuid.py
+++ b/Lib/uuid.py
@@ -134,7 +134,7 @@ class UUID:
                     uuid_generate_time_safe(3).
     """
 
-    __slots__ = ('int', 'is_safe', '__weakref__')
+    __slots__ = ('__weakref__', 'int', 'is_safe')
 
     def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None,
                        int=None, version=None,
diff --git a/Lib/weakref.py b/Lib/weakref.py
index 25b70927e2..1d20272ce3 100644
--- a/Lib/weakref.py
+++ b/Lib/weakref.py
@@ -41,7 +41,7 @@ class WeakMethod(ref):
     a bound method, working around the lifetime problem of bound methods.
     """
 
-    __slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__"
+    __slots__ = "__weakref__", "_alive", "_func_ref", "_meth_type"
 
     def __new__(cls, meth, callback=None):
         try:
@@ -563,7 +563,7 @@ class finalize:
     _registered_with_atexit = False
 
     class _Info:
-        __slots__ = ("weakref", "func", "args", "kwargs", "atexit", "index")
+        __slots__ = ("args", "atexit", "func", "index", "kwargs", "weakref")
 
     def __init__(self, obj, func, /, *args, **kwargs):
         if not self._registered_with_atexit:
diff --git a/Lib/xml/dom/expatbuilder.py b/Lib/xml/dom/expatbuilder.py
index 7dd667bf3f..812e00f1b2 100644
--- a/Lib/xml/dom/expatbuilder.py
+++ b/Lib/xml/dom/expatbuilder.py
@@ -509,7 +509,7 @@ def acceptNode(self, node):
 
 
 class FilterCrutch(object):
-    __slots__ = '_builder', '_level', '_old_start', '_old_end'
+    __slots__ = '_builder', '_level', '_old_end', '_old_start'
 
     def __init__(self, builder):
         self._level = 0
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
index db51f350ea..7aed19c1c2 100644
--- a/Lib/xml/dom/minidom.py
+++ b/Lib/xml/dom/minidom.py
@@ -656,7 +656,7 @@ def __setstate__(self, state):
 
 
 class TypeInfo(object):
-    __slots__ = 'namespace', 'name'
+    __slots__ = 'name', 'namespace'
 
     def __init__(self, namespace, name):
         self.namespace = namespace
@@ -1007,7 +1007,7 @@ def replaceChild(self, newChild, oldChild):
 
 class ProcessingInstruction(Childless, Node):
     nodeType = Node.PROCESSING_INSTRUCTION_NODE
-    __slots__ = ('target', 'data')
+    __slots__ = ('data', 'target')
 
     def __init__(self, target, data):
         self.target = target
@@ -1032,7 +1032,7 @@ def writexml(self, writer, indent="", addindent="", newl=""):
 
 
 class CharacterData(Childless, Node):
-    __slots__=('_data', 'ownerDocument','parentNode', 'previousSibling', 'nextSibling')
+    __slots__=('_data', 'nextSibling', 'ownerDocument', 'parentNode', 'previousSibling')
 
     def __init__(self):
         self.ownerDocument = self.parentNode = None
diff --git a/Lib/zoneinfo/_zoneinfo.py b/Lib/zoneinfo/_zoneinfo.py
index b77dc0ed39..01f1303540 100644
--- a/Lib/zoneinfo/_zoneinfo.py
+++ b/Lib/zoneinfo/_zoneinfo.py
@@ -394,7 +394,7 @@ def _ts_to_local(trans_idx, trans_list_utc, utcoffsets):
 
 
 class _ttinfo:
-    __slots__ = ["utcoff", "dstoff", "tzname"]
+    __slots__ = ["dstoff", "tzname", "utcoff"]
 
     def __init__(self, utcoff, dstoff, tzname):
         self.utcoff = utcoff
@@ -514,7 +514,7 @@ def _post_epoch_days_before_year(year):
 
 
 class _DayOffset:
-    __slots__ = ["d", "julian", "hour", "minute", "second"]
+    __slots__ = ["d", "hour", "julian", "minute", "second"]
 
     def __init__(self, d, julian, hour=2, minute=0, second=0):
         min_day = 0 + julian  # convert bool to int
@@ -541,7 +541,7 @@ def year_to_epoch(self, year):
 
 
 class _CalendarOffset:
-    __slots__ = ["m", "w", "d", "hour", "minute", "second"]
+    __slots__ = ["d", "hour", "m", "minute", "second", "w"]
 
     _DAYS_BEFORE_MONTH = (
         -1,
diff --git a/Tools/c-analyzer/c_common/clsutil.py b/Tools/c-analyzer/c_common/clsutil.py
index aa5f6b9831..8b61321355 100644
--- a/Tools/c-analyzer/c_common/clsutil.py
+++ b/Tools/c-analyzer/c_common/clsutil.py
@@ -9,7 +9,7 @@ class Slot:
     e.g. tuple subclasses.
     """
 
-    __slots__ = ('initial', 'default', 'readonly', 'instances', 'name')
+    __slots__ = ('default', 'initial', 'instances', 'name', 'readonly')
 
     def __init__(self, initial=_NOT_SET, *,
                  default=_NOT_SET,

The following 17 violations were flagged but left unfixed by the rule as it currently stands. (They were unfixed because they're multiline definitions):

cpython\Lib\asyncio\events.py:32:17: RUF023 `Handle.__slots__` is not sorted
cpython\Lib\dataclasses.py:296:17: RUF023 `Field.__slots__` is not sorted
cpython\Lib\dataclasses.py:360:17: RUF023 `_DataclassParams.__slots__` is not sorted
cpython\Lib\pathlib\__init__.py:87:17: RUF023 `PurePath.__slots__` is not sorted
cpython\Lib\pickletools.py:175:17: RUF023 `ArgumentDescriptor.__slots__` is not sorted
cpython\Lib\pickletools.py:949:17: RUF023 `StackObject.__slots__` is not sorted
cpython\Lib\pickletools.py:1095:17: RUF023 `OpcodeInfo.__slots__` is not sorted
cpython\Lib\test\test_inspect\test_inspect.py:526:17: RUF023 `SlotUser.__slots__` is not sorted
cpython\Lib\traceback.py:313:17: RUF023 `FrameSummary.__slots__` is not sorted
cpython\Lib\typing.py:857:17: RUF023 `ForwardRef.__slots__` is not sorted
cpython\Lib\xml\dom\minidom.py:362:15: RUF023 `Attr.__slots__` is not sorted
cpython\Lib\xml\dom\minidom.py:681:15: RUF023 `Element.__slots__` is not sorted
cpython\Lib\xml\dom\minidom.py:1563:17: RUF023 `Document.__slots__` is not sorted
cpython\Lib\xml\dom\xmlbuilder.py:257:17: RUF023 `DOMInputSource.__slots__` is not sorted
cpython\Lib\zipfile\__init__.py:377:17: RUF023 `ZipInfo.__slots__` is not sorted
cpython\Lib\zoneinfo\_common.py:128:17: RUF023 `_TZifHeader.__slots__` is not sorted
cpython\Lib\zoneinfo\_zoneinfo.py:422:17: RUF023 `_TZStr.__slots__` is not sorted

@AlexWaygood AlexWaygood added the rule Implementing or modifying a lint rule label Jan 17, 2024
Copy link
Contributor

github-actions bot commented Jan 17, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+150 -0 violations, +0 -0 fixes in 3 projects; 40 projects unchanged)

DisnakeDev/disnake (+111 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --preview

+ disnake/abc.py:201:17: RUF023 [*] `_Overwrites.__slots__` is not sorted
+ disnake/activity.py:289:17: RUF023 [*] `Activity.__slots__` is not sorted
+ disnake/activity.py:530:17: RUF023 [*] `Streaming.__slots__` is not sorted
+ disnake/activity.py:619:17: RUF023 [*] `Spotify.__slots__` is not sorted
+ disnake/activity.py:803:17: RUF023 [*] `CustomActivity.__slots__` is not sorted
+ disnake/app_commands.py:1017:17: RUF023 [*] `ApplicationCommandPermissions.__slots__` is not sorted
+ disnake/app_commands.py:1075:17: RUF023 [*] `GuildApplicationCommandPermissions.__slots__` is not sorted
+ disnake/app_commands.py:241:17: RUF023 [*] `Option.__slots__` is not sorted
+ disnake/appinfo.py:156:17: RUF023 [*] `AppInfo.__slots__` is not sorted
+ disnake/appinfo.py:297:17: RUF023 [*] `PartialAppInfo.__slots__` is not sorted
+ disnake/appinfo.py:43:17: RUF023 [*] `InstallParams.__slots__` is not sorted
+ disnake/application_role_connection.py:50:17: RUF023 [*] `ApplicationRoleConnectionMetadata.__slots__` is not sorted
+ disnake/asset.py:190:34: RUF023 [*] `Asset.__slots__` is not sorted
+ disnake/automod.py:283:17: RUF023 [*] `AutoModTriggerMetadata.__slots__` is not sorted
+ disnake/automod.py:455:17: RUF023 [*] `AutoModRule.__slots__` is not sorted
+ disnake/automod.py:729:17: RUF023 [*] `AutoModActionExecution.__slots__` is not sorted
+ disnake/automod.py:83:17: RUF023 [*] `AutoModAction.__slots__` is not sorted
+ disnake/channel.py:1101:17: RUF023 [*] `VocalGuildChannel.__slots__` is not sorted
+ disnake/channel.py:1291:17: RUF023 [*] `VoiceChannel.__slots__` is not sorted
+ disnake/channel.py:179:17: RUF023 [*] `TextChannel.__slots__` is not sorted
+ disnake/channel.py:1947:17: RUF023 [*] `StageChannel.__slots__` is not sorted
+ disnake/channel.py:2737:17: RUF023 [*] `CategoryChannel.__slots__` is not sorted
+ disnake/channel.py:3226:17: RUF023 [*] `ForumChannel.__slots__` is not sorted
+ disnake/channel.py:4222:17: RUF023 [*] `DMChannel.__slots__` is not sorted
+ disnake/channel.py:4388:17: RUF023 [*] `GroupChannel.__slots__` is not sorted
+ disnake/client.py:173:34: RUF023 [*] `SessionStartLimit.__slots__` is not sorted
+ disnake/components.py:189:34: RUF023 [*] `Button.__slots__` is not sorted
+ disnake/components.py:269:34: RUF023 [*] `BaseSelectMenu.__slots__` is not sorted
+ disnake/components.py:535:34: RUF023 [*] `SelectOption.__slots__` is not sorted
+ disnake/components.py:646:34: RUF023 [*] `TextInput.__slots__` is not sorted
+ disnake/embeds.py:170:17: RUF023 [*] `Embed.__slots__` is not sorted
+ disnake/emoji.py:76:34: RUF023 [*] `Emoji.__slots__` is not sorted
+ disnake/entitlement.py:81:17: RUF023 [*] `Entitlement.__slots__` is not sorted
+ disnake/ext/commands/cooldowns.py:282:17: RUF023 [*] `_Semaphore.__slots__` is not sorted
+ disnake/ext/commands/cooldowns.py:330:17: RUF023 [*] `MaxConcurrency.__slots__` is not sorted
+ disnake/ext/commands/cooldowns.py:71:17: RUF023 [*] `Cooldown.__slots__` is not sorted
+ disnake/ext/tasks/__init__.py:53:17: RUF023 [*] `SleepHandle.__slots__` is not sorted
... 74 additional changes omitted for project

apache/airflow (+2 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --preview --select ALL

+ airflow/io/path.py:88:17: RUF023 [*] `ObjectStoragePath.__slots__` is not sorted
+ airflow/providers_manager.py:100:17: RUF023 [*] `LazyDictWithCache.__slots__` is not sorted

ibis-project/ibis (+37 -0 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --preview

+ ibis/backends/pandas/aggcontext.py:254:17: RUF023 [*] `AggregationContext.__slots__` is not sorted
+ ibis/common/annotations.py:103:17: RUF023 [*] `Annotation.__slots__` is not sorted
+ ibis/common/annotations.py:200:17: RUF023 [*] `Argument.__slots__` is not sorted
+ ibis/common/annotations.py:40:17: RUF023 [*] `AttributeValidationError.__slots__` is not sorted
+ ibis/common/annotations.py:52:17: RUF023 [*] `ReturnValidationError.__slots__` is not sorted
+ ibis/common/annotations.py:64:17: RUF023 [*] `SignatureValidationError.__slots__` is not sorted
+ ibis/common/collections.py:288:17: RUF023 [*] `FrozenDict.__slots__` is not sorted
+ ibis/common/collections.py:347:17: RUF023 [*] `RewindableIterator.__slots__` is not sorted
+ ibis/common/deferred.py:324:17: RUF023 [*] `Attr.__slots__` is not sorted
+ ibis/common/deferred.py:344:17: RUF023 [*] `Item.__slots__` is not sorted
+ ibis/common/deferred.py:378:17: RUF023 [*] `Call.__slots__` is not sorted
+ ibis/common/deferred.py:442:17: RUF023 [*] `UnaryOperator.__slots__` is not sorted
... 25 additional changes omitted for project

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
RUF023 150 150 0 0 0

@AlexWaygood AlexWaygood changed the title [Proof of concept] Add a rule/autofix to sort __slots__ and __match_args__ Add a rule/autofix to sort __slots__ and __match_args__ Jan 18, 2024
@AlexWaygood
Copy link
Member Author

Okay, after the latest push, we now fix all but one violation in CPython (there's only one place in CPython where a multiline dict is used for __slots__). Here's the diff from the autofix running on CPython: cpython_diff.txt

Copy link

codspeed-hq bot commented Jan 18, 2024

CodSpeed Performance Report

Merging #9564 will not alter performance

Comparing AlexWaygood:sort-dunder-slots (333fe62) with main (a3d667e)

Summary

✅ 30 untouched benchmarks

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. My only concern is that the use of impl FnMut for the comparator leads multiple copies of the same code because of monomorpization. I recommend introducing an enum with variants for the two sort options that you need.

}

#[derive(Debug)]
struct NodeAnalysis<'a> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The name here isn't telling me much. What kind of analysis is it? Is it like the SortDunderAnalysis? Or can we give that structure another name, because the structure isn't the analysis. What does it represent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed a rename and some more docs in cf6ca75 -- any better?

I really struggled with naming this the other day, but coming back to it after a few days made it seem obvious :)

@Bibo-Joshi
Copy link

I'm really astonished how quickly you closed #1198 and brought to live this follow-up PR - big kudos!

Implementing an autofix for multiline __match_args__ wouldn't be too hard […]. The same is true for __slots__ when it's a tuple, list or set. I don't feel enthused about implementing an autofix for multiline __slots__ definitions where __slots__ is a dict, however. […]

If we're interested in autofixes for multiline __slots__ and __match_args__ definitions, let me know whether you'd prefer it in this PR or a followup!

Just wanted to let you know that I'd be very interested in autofixes for multline tuple- __slots__. Not implementing that for dicts is completely understandable IMO (and I don't have a use case etiher …)

@AlexWaygood
Copy link
Member Author

Just wanted to let you know that I'd be very interested in autofixes for multline tuple- __slots__. Not implementing that for dicts is completely understandable IMO (and I don't have a use case etiher …)

Have no fear, the description in my PR summary is already slightly out of date — I've already implemented the autofix for multiline tuple __slots__ ;)

Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, a couple questions and comments!

}
}

fn create_fix(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a method on StringLiteralDisplay?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure... it makes the signature cleaner, but I was thinking of this struct as just a pretty simple container for heterogenous data; this breaks my mental model of that slightly. Anyway, I implemented it as a method in e64879a :)

}
}

impl Eq for IsortSortKey<'_> {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BurntSushi - Do these Ord / PartialOrd / PartialEq / Eq implementations look right / consistent to you? (I've never properly learned the relationships between them.)

Copy link
Member Author

@AlexWaygood AlexWaygood Jan 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(FWIW: these haven't changed in this PR, just moved to a different file, and Micha reviewed these in my previous PR (my initial implementations in my previous PR had some issues, which Micha pointed out))

where
F: FnMut(&str, &str) -> Ordering,
{
assert_eq!(elts.len(), elements.len());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think he's suggesting creating a dedicated struct like that. Since you're allocating below anyway with collect_vec, perhaps you want to do something like:

struct Whatever<'a>(Vec<(&'a Expr, &'a str)>)

That is: store the zipped elements, so that the constraint is always enforced once you've created the struct.

}
}

/// Collect data on each line of a multiline string sequence.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much of the below is new code vs. moved from the isort rule?

Copy link
Member Author

@AlexWaygood AlexWaygood Jan 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think everything below this point is entirely unchanged, just moved, except that a few things have been renamed and a few comments tweaked (e.g. collect_dunder_all_lines() has been renamed to collect_string_sequence_lines(), etc.)

@AlexWaygood AlexWaygood changed the title Add a rule/autofix to sort __slots__ and __match_args__ Add a rule/autofix to sort __slots__ and __match_args__ Jan 22, 2024
Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

// Safe from underflow, as the constructor guarantees
// that the underlying vector has length >= 2
self.0.len() - 1
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I'd find it clearer to just have .len() on this and do if i < element_trios.len() - 1, since that's such a common pattern in iteration.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely know what you mean, but here specifically I do quite like having this logic right next to the assertions in the constructor method that maintain the invariant that protect us from underflow here

@AlexWaygood
Copy link
Member Author

AlexWaygood commented Jan 22, 2024

Thanks both for the reviews! :D

I've had two approvals, so I'm landing this now, but very happy to tackle any further comments in followup PRs :)

@AlexWaygood AlexWaygood enabled auto-merge (squash) January 22, 2024 12:19
@AlexWaygood AlexWaygood merged commit f5061db into astral-sh:main Jan 22, 2024
16 checks passed
@AlexWaygood AlexWaygood deleted the sort-dunder-slots branch January 22, 2024 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rule Implementing or modifying a lint rule
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants