Repository: jazzband/pip-tools

Issue:
Drop Python 3.6 support
#### What's the problem this feature will solve?

Python 3.6 is EOL since December 2021

#### Describe the solution you'd like

Drop Python 3.6 support in pip-tools

#### Additional context

There's already a related PR #1560  

Drop support for EOL Python 3.6
<!--- Describe the changes here. --->

##### Contributor checklist

- [ ] Provided the tests for the changes.
- [x] Assure PR title is short, clear, and good to be included in the user-oriented changelog

##### Maintainer checklist

- [ ] Assure one of these labels is present: `backwards incompatible`, `feature`, `enhancement`, `deprecation`, `bug`, `dependency`, `docs` or `skip-changelog` as they determine changelog listing.
- [ ] Assign the PR to an existing or new milestone for the target version (following [Semantic Versioning](https://blog.versioneye.com/2014/01/16/semantic-versioning/)).

---

Fixes https://github.com/jazzband/pip-tools/issues/1562.

Includes and closes https://github.com/jazzband/pip-tools/pull/1560.
Includes and closes https://github.com/jazzband/pip-tools/pull/1561.


Discussion:

Resolving patch:
diff --git a/piptools/_compat/contextlib.py b/piptools/_compat/contextlib.py
deleted file mode 100644
--- a/piptools/_compat/contextlib.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Ported from python 3.7 contextlib.py
-from types import TracebackType
-from typing import Optional, Type, TypeVar
-
-_T = TypeVar("_T")
-
-
-class nullcontext:
-    """Context manager that does no additional processing.
-    Used as a stand-in for a normal context manager, when a particular
-    block of code is only sometimes used with a normal context manager:
-    cm = optional_cm if condition else nullcontext()
-    with cm:
-        # Perform operation, using optional_cm if condition is True
-
-    TODO: replace with `contextlib.nullcontext()` after Python 3.6 being dropped
-    """
-
-    def __init__(self, enter_result: _T) -> None:
-        self.enter_result = enter_result
-
-    def __enter__(self) -> _T:
-        return self.enter_result
-
-    def __exit__(
-        self,
-        exc_type: Optional[Type[BaseException]],
-        exc_val: Optional[BaseException],
-        exc_tb: Optional[TracebackType],
-    ) -> Optional[bool]:
-        pass
diff --git a/piptools/repositories/pypi.py b/piptools/repositories/pypi.py
--- a/piptools/repositories/pypi.py
+++ b/piptools/repositories/pypi.py
@@ -1,3 +1,4 @@
+import contextlib
 import hashlib
 import itertools
 import logging
@@ -39,7 +40,7 @@
 from pip._vendor.packaging.version import _BaseVersion
 from pip._vendor.requests import RequestException, Session
 
-from .._compat import contextlib
+from .._compat import PIP_VERSION
 from ..exceptions import NoCandidateFound
 from ..logging import log
 from ..utils import (
@@ -103,7 +104,8 @@ def __init__(self, pip_args: List[str], cache_dir: str):
         self._cache_dir = normalize_path(str(cache_dir))
         self._download_dir = os.path.join(self._cache_dir, "pkgs")
 
-        self._setup_logging()
+        if PIP_VERSION[0] < 22:
+            self._setup_logging()
 
     def clear_caches(self) -> None:
         rmtree(self._download_dir, ignore_errors=True)
@@ -439,7 +441,8 @@ def _wheel_support_index_min(self: Wheel, tags: List[Tag]) -> int:
     def _setup_logging(self) -> None:
         """
         Setup pip's logger. Ensure pip is verbose same as pip-tools and sync
-        pip's log stream with LogContext.stream.
+        pip's log stream with LogContext.stream. This is only necessary for
+        pip<22.0.
         """
         # Default pip's logger is noisy, so decrease it's verbosity
         setup_logging(
diff --git a/piptools/utils.py b/piptools/utils.py
--- a/piptools/utils.py
+++ b/piptools/utils.py
@@ -1,4 +1,5 @@
 import collections
+import copy
 import itertools
 import json
 import os
@@ -26,6 +27,7 @@
 from pip._internal.vcs import is_url
 from pip._vendor.packaging.markers import Marker
 from pip._vendor.packaging.specifiers import SpecifierSet
+from pip._vendor.packaging.utils import canonicalize_name
 from pip._vendor.packaging.version import Version
 from pip._vendor.pkg_resources import Distribution, Requirement, get_distribution
 
@@ -121,7 +123,11 @@ def format_requirement(
     elif is_url_requirement(ireq):
         line = _build_direct_reference_best_efforts(ireq)
     else:
-        line = str(ireq.req).lower()
+        # Canonicalize the requirement name
+        # https://packaging.pypa.io/en/latest/utils.html#packaging.utils.canonicalize_name
+        req = copy.copy(ireq.req)
+        req.name = canonicalize_name(req.name)
+        line = str(req)
 
     if marker:
         line = f"{line} ; {marker}"


Test patch:
diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py
--- a/tests/test_cli_compile.py
+++ b/tests/test_cli_compile.py
@@ -610,7 +610,7 @@ def test_url_package(runner, line, dependency, generate_hashes):
         ),
         pytest.param(
             path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_subdir"))
-            + "#subdirectory=subdir&egg=small_fake_a",
+            + "#subdirectory=subdir&egg=small-fake-a",
             "small-fake-a @ "
             + path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_subdir"))
             + "#subdirectory=subdir",
diff --git a/tests/test_writer.py b/tests/test_writer.py
--- a/tests/test_writer.py
+++ b/tests/test_writer.py
@@ -381,7 +381,8 @@ def test_write_find_links(writer, find_links, expected_lines):
 
 def test_write_order(writer, from_line):
     """
-    Order of packages should match that of `pip freeze`.
+    Order of packages should match that of `pip freeze`, with the exception
+    that requirement names should be canonicalized.
     """
     writer.emit_header = False
 
@@ -393,7 +394,7 @@ def test_write_order(writer, from_line):
     ]
     expected_lines = [
         "package==5.6",
-        "package_a==0.1",
+        "package-a==0.1",
         "package-b==2.3.4",
         "package2==7.8.9",
     ]