Repository: bwhmather/ssort

Issue:
Skip files listed in .gitignore
I ran `ssort` at the root of my project and it started sorting the files in my `venv` directory. (incidentally, this threw on `venv/lib/python3.10/site-packages/blib2to3/pgen2/parse.py`)

Hints from issue comments:
@bwhmather: I think this is a good idea.  It also matches what black and isort do, which is important.

Relevant bits of code from black:
  - https://github.com/psf/black/blob/b4a6bb08fa704facbf3397f95b3216e13c3c964a/src/black/files.py
  
Relevant bits of code from isort:
  - https://github.com/PyCQA/isort/blob/c6a41965247a858a0afd848fbebfca18b8983917/isort/files.py
  - https://github.com/PyCQA/isort/blob/c6a41965247a858a0afd848fbebfca18b8983917/isort/settings.py#L588-L637
  
The black module looks particularly straightforward and the project is license compatible, so it might make sense to just copy the whole lot in.
  @bwhmather: I found a version of `blib2to3` in the black project, but it doesn't trigger any errors for me.  Would you be able to upload your copy?  It sounds like a good candidate for the examples directory.@jgberry: @bwhmather I'm taking a crack at this based on the work done in the `exclude` branch. It seems that, at a bare minimum, we'll need to add a dependency on `pathspec` to handle `.gitignore` parsing. What are your thoughts on 3rd party dependencies?@bwhmather: Let's not go full npm, but I have no problem with pulling in pathspec.  I would expect that 100% of people who install ssort will also install black, which means that if pathspec's developer decides to start using it to mine bitcoin then the battle is already lost.@bwhmather: Released as part of 0.11.3.  Thank you @jgberry 

Solution patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index cd2875d..14a3e68 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -11,7 +11,7 @@ jobs:
     runs-on: ubuntu-18.04
     strategy:
       matrix:
-        python-version: [3.8, 3.9]
+        python-version: ["3.8", "3.9", "3.10"]
     steps:
     - uses: actions/checkout@v2
     - name: Set up Python ${{ matrix.python-version }}
@@ -32,10 +32,10 @@ jobs:
     runs-on: ubuntu-18.04
     steps:
     - uses: actions/checkout@v2
-    - name: Set up Python 3.9
+    - name: Set up Python 3.10
       uses: actions/setup-python@v2
       with:
-        python-version: 3.9
+        python-version: "3.10"
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
@@ -49,10 +49,10 @@ jobs:
     runs-on: ubuntu-18.04
     steps:
     - uses: actions/checkout@v2
-    - name: Set up Python 3.9
+    - name: Set up Python 3.10
       uses: actions/setup-python@v2
       with:
-        python-version: 3.9
+        python-version: "3.10"
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
@@ -66,10 +66,10 @@ jobs:
     runs-on: ubuntu-18.04
     steps:
     - uses: actions/checkout@v2
-    - name: Set up Python 3.9
+    - name: Set up Python 3.10
       uses: actions/setup-python@v2
       with:
-        python-version: 3.9
+        python-version: "3.10"
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
@@ -83,10 +83,10 @@ jobs:
     runs-on: ubuntu-18.04
     steps:
     - uses: actions/checkout@v2
-    - name: Set up Python 3.9
+    - name: Set up Python 3.10
       uses: actions/setup-python@v2
       with:
-        python-version: 3.9
+        python-version: "3.10"
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
@@ -100,10 +100,10 @@ jobs:
     runs-on: ubuntu-18.04
     steps:
     - uses: actions/checkout@v2
-    - name: Set up Python 3.9
+    - name: Set up Python 3.10
       uses: actions/setup-python@v2
       with:
-        python-version: 3.9
+        python-version: "3.10"
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
diff --git a/setup.cfg b/setup.cfg
index 885a4f7..2b807b7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,6 +22,7 @@ classifiers =
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
     Topic :: Software Development :: Libraries :: Python Modules
     Topic :: Software Development :: Quality Assurance
 
diff --git a/src/ssort/_ast.py b/src/ssort/_ast.py
index c81a276..e39b21c 100644
--- a/src/ssort/_ast.py
+++ b/src/ssort/_ast.py
@@ -439,7 +439,6 @@ def _iter_child_nodes_of_withitem(node: ast.withitem) -> Iterable[ast.AST]:
 
 if sys.version_info >= (3, 10):
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.match_case)
     def _iter_child_nodes_of_match_case(
         node: ast.match_case,
@@ -449,28 +448,24 @@ if sys.version_info >= (3, 10):
             yield node.guard
         yield from node.body
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.MatchValue)
     def _iter_child_nodes_of_match_value(
         node: ast.MatchValue,
     ) -> Iterable[ast.AST]:
         yield node.value
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.MatchSingleton)
     def _iter_child_nodes_of_match_singleton(
         node: ast.MatchSingleton,
     ) -> Iterable[ast.AST]:
         return ()
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.MatchSequence)
     def _iter_child_nodes_of_match_sequence(
         node: ast.MatchSequence,
     ) -> Iterable[ast.AST]:
         yield from node.patterns
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.MatchMapping)
     def _iter_child_nodes_of_match_mapping(
         node: ast.MatchMapping,
@@ -478,7 +473,6 @@ if sys.version_info >= (3, 10):
         yield from node.keys
         yield from node.patterns
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.MatchClass)
     def _iter_child_nodes_of_match_class(
         node: ast.MatchClass,
@@ -487,20 +481,17 @@ if sys.version_info >= (3, 10):
         yield from node.patterns
         yield from node.kwd_patterns
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.MatchStar)
     def _iter_child_nodes_of_match_star(
         node: ast.MatchStar,
     ) -> Iterable[ast.AST]:
         return ()
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.MatchAs)
     def _iter_child_nodes_of_match_as(node: ast.MatchAs) -> Iterable[ast.AST]:
         if node.pattern is not None:
             yield node.pattern
 
-    # pylint: disable=no-member
     @iter_child_nodes.register(ast.MatchOr)
     def _iter_child_nodes_of_match_or(node: ast.MatchOr) -> Iterable[ast.AST]:
         yield from node.patterns
diff --git a/src/ssort/_bindings.py b/src/ssort/_bindings.py
index 5e2997a..61c79f0 100644
--- a/src/ssort/_bindings.py
+++ b/src/ssort/_bindings.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import ast
+import sys
 from typing import Iterable
 
 from ssort._ast import iter_child_nodes, node_dispatch
@@ -80,3 +81,29 @@ def _get_bindings_for_except_handler(node: ast.ExceptHandler) -> Iterable[str]:
         yield node.name
     for statement in node.body:
         yield from get_bindings(statement)
+
+
+if sys.version_info >= (3, 10):
+
+    @get_bindings.register(ast.MatchStar)
+    def _get_bindings_for_match_star(node: ast.MatchStar) -> Iterable[str]:
+        if node.name is not None:
+            yield node.name
+
+    @get_bindings.register(ast.MatchMapping)
+    def _get_bindings_for_match_mapping(
+        node: ast.MatchMapping,
+    ) -> Iterable[str]:
+        for key in node.keys:
+            yield from get_bindings(key)
+        for pattern in node.patterns:
+            yield from get_bindings(pattern)
+        if node.rest is not None:
+            yield node.rest
+
+    @get_bindings.register(ast.MatchAs)
+    def _get_bindings_for_match_as(node: ast.MatchAs) -> Iterable[str]:
+        if node.pattern is not None:
+            yield from get_bindings(node.pattern)
+        if node.name is not None:
+            yield node.name
diff --git a/src/ssort/_builtins.py b/src/ssort/_builtins.py
index a0e644d..48385af 100644
--- a/src/ssort/_builtins.py
+++ b/src/ssort/_builtins.py
@@ -25,6 +25,7 @@ MODULE_BUILTINS = {
     "DeprecationWarning",
     "EOFError",
     "Ellipsis",
+    "EncodingWarning",
     "EnvironmentError",
     "Exception",
     "False",
@@ -83,7 +84,9 @@ MODULE_BUILTINS = {
     "WindowsError",
     "ZeroDivisionError",
     "abs",
+    "aiter",
     "all",
+    "anext",
     "any",
     "apply",
     "ascii",
diff --git a/tox.ini b/tox.ini
index 0ff0ea9..4492af4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,11 @@
 [tox]
-envlist = py38,py39,black,isort,ssort,pyflakes,pylint
+envlist = py38,py39,py310,black,isort,ssort,pyflakes,pylint
 
 [testenv]
 deps =
     pytest
 commands =
-    pytest -v tests
+    pytest -vv tests
 
 [testenv:black]
 deps =


Test patch:
diff --git a/tests/test_bindings.py b/tests/test_bindings.py
index a1d4d9f..220f10d 100644
--- a/tests/test_bindings.py
+++ b/tests/test_bindings.py
@@ -14,6 +14,12 @@ walrus_operator = pytest.mark.skipif(
 )
 
 
+match_statement = pytest.mark.skipif(
+    sys.version_info < (3, 10),
+    reason="match statements were introduced in python 3.10",
+)
+
+
 def _parse(source):
     source = textwrap.dedent(source)
     root = ast.parse(source)
@@ -1409,3 +1415,135 @@ def test_formatted_value_bindings_walrus():
 def test_formatted_value_bindings_format_spec_walrus():
     node = _parse("f'{a} {b:{0 + (c := 0.3)}} {d}'")
     assert list(get_bindings(node)) == ["c"]
+
+
+@match_statement
+def test_match_statement_bindings_literal():
+    node = _parse(
+        """
+        match a:
+            case True:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == []
+
+
+@match_statement
+def test_match_statement_bindings_capture():
+    node = _parse(
+        """
+        match a:
+            case b:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == ["b"]
+
+
+@match_statement
+def test_match_statement_bindings_wildcard():
+    node = _parse(
+        """
+        match a:
+            case _:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == []
+
+
+@match_statement
+def test_match_statement_bindings_constant():
+    node = _parse(
+        """
+        match a:
+            case 1:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == []
+
+
+@match_statement
+def test_match_statement_bindings_named_constant():
+    node = _parse(
+        """
+        match a:
+            case MyEnum.CONSTANT:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == []
+
+
+@match_statement
+def test_match_statement_bindings_sequence():
+    node = _parse(
+        """
+        match a:
+            case [b, *c, d, _]:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == ["b", "c", "d"]
+
+
+@match_statement
+def test_match_statement_bindings_sequence_wildcard():
+    node = _parse(
+        """
+        match a:
+            case [*_]:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == []
+
+
+@match_statement
+def test_match_statement_bindings_mapping():
+    node = _parse(
+        """
+        match a:
+            case {"k1": "v1", "k2": b, "k3": _, **c}:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == ["b", "c"]
+
+
+@match_statement
+def test_match_statement_bindings_class():
+    node = _parse(
+        """
+        match a:
+            case MyClass(0, b, x=_, y=c):
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == ["b", "c"]
+
+
+@match_statement
+def test_match_statement_bindings_or():
+    node = _parse(
+        """
+        match a:
+            case b | c:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == ["b", "c"]
+
+
+@match_statement
+def test_match_statement_bindings_as():
+    node = _parse(
+        """
+        match a:
+            case b as c:
+                pass
+        """
+    )
+    assert list(get_bindings(node)) == ["b", "c"]
diff --git a/tests/test_method_requirements.py b/tests/test_method_requirements.py
index b312d00..b9ed1f3 100644
--- a/tests/test_method_requirements.py
+++ b/tests/test_method_requirements.py
@@ -2,8 +2,15 @@ import ast
 import sys
 import textwrap
 
+import pytest
+
 from ssort._method_requirements import get_method_requirements
 
+match_statement = pytest.mark.skipif(
+    sys.version_info < (3, 10),
+    reason="match statements were introduced in python 3.10",
+)
+
 
 def _method_requirements(source):
     source = textwrap.dedent(source)
@@ -295,3 +302,16 @@ def test_method_requirements_ann_assign():
         """
     )
     assert reqs == ["b", "c"]
+
+
+@match_statement
+def test_method_requirements_match_statement():
+    reqs = _method_requirements(
+        """
+        def fun(self):
+            match self.a:
+                case self.b:
+                    pass
+        """
+    )
+    assert reqs == ["a", "b"]
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
index 3d9bf8a..c63e01f 100644
--- a/tests/test_requirements.py
+++ b/tests/test_requirements.py
@@ -2,8 +2,15 @@ import ast
 import sys
 import textwrap
 
+import pytest
+
 from ssort._requirements import get_requirements
 
+match_statement = pytest.mark.skipif(
+    sys.version_info < (3, 10),
+    reason="match statements were introduced in python 3.10",
+)
+
 
 def _parse(source):
     source = textwrap.dedent(source)
@@ -1028,3 +1035,123 @@ def test_slice_requirements():
 
     """
     pass
+
+
+@match_statement
+def test_match_statement_requirements_literal():
+    node = _parse(
+        """
+        match a:
+            case True:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a"]
+
+
+@match_statement
+def test_match_statement_requirements_capture():
+    node = _parse(
+        """
+        match a:
+            case b:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a"]
+
+
+@match_statement
+def test_match_statement_requirements_wildcard():
+    node = _parse(
+        """
+        match a:
+            case _:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a"]
+
+
+@match_statement
+def test_match_statement_requirements_constant():
+    node = _parse(
+        """
+        match a:
+            case 1:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a"]
+
+
+@match_statement
+def test_match_statement_requirements_named_constant():
+    node = _parse(
+        """
+        match a:
+            case MyEnum.CONSTANT:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a", "MyEnum"]
+
+
+@match_statement
+def test_match_statement_requirements_sequence():
+    node = _parse(
+        """
+        match a:
+            case [b, *c, d, _]:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a"]
+
+
+@match_statement
+def test_match_statement_requirements_mapping():
+    node = _parse(
+        """
+        match a:
+            case {"k1": "v1", "k2": b, "k3": _, **c}:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a"]
+
+
+@match_statement
+def test_match_statement_requirements_class():
+    node = _parse(
+        """
+        match a:
+            case MyClass(0, b, x=_, y=c):
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a", "MyClass"]
+
+
+@match_statement
+def test_match_statement_requirements_or():
+    node = _parse(
+        """
+        match a:
+            case b | c:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a"]
+
+
+@match_statement
+def test_match_statement_requirements_as():
+    node = _parse(
+        """
+        match a:
+            case b as c:
+                pass
+        """
+    )
+    assert _dep_names(node) == ["a"]