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

Update mypy to v0.770 #8235

Merged
merged 3 commits into from
May 18, 2020
Merged

Conversation

deveshks
Copy link
Contributor

@deveshks deveshks commented May 13, 2020

As mentioned in #8231 (comment), adding --pretty to the mypy args make the mypy errors less readable.

This PR upgrades mypy to v0.770 which fixes this issue in python/mypy#8145

.pre-commit-config.yaml Outdated Show resolved Hide resolved
@deveshks deveshks force-pushed the remove-pretty-arg-from-mypy branch from d82eabe to 31fba02 Compare May 13, 2020 20:52
@deveshks deveshks force-pushed the remove-pretty-arg-from-mypy branch from 31fba02 to 4ff0f56 Compare May 13, 2020 21:05
@deveshks
Copy link
Contributor Author

For some reason, the tests which interact with external svn and hg repositories and running in the azure pipeline are timing out, causing the checks to fail.

I think this is unrelated to the changes made here.

@deveshks
Copy link
Contributor Author

deveshks commented May 13, 2020

As per python/mypy#8816 (comment) , seems like v0.770 fixed this, so we have a few options to move this PR ahead.

  1. Leave the changes as-is.
  2. Upgrade to 0.770, and leave the --pretty option removed
  3. Upgrade to 0.770, and readd the --pretty option

Let me know what we want to do here, and I will add another commit in this PR accordingly to make either of the 3 options happen.

@pradyunsg
Copy link
Member

Ah well. Let's bump to 0.770 and see what happens. :)

@deveshks deveshks force-pushed the remove-pretty-arg-from-mypy branch from 4ff0f56 to feaa76a Compare May 14, 2020 05:10
@deveshks
Copy link
Contributor Author

deveshks commented May 14, 2020

Ah well. Let's bump to 0.770 and see what happens. :)

Update mypy to 0.770 in latest commit and readded --pretty.

Edit: This raised a new error while running mypy checks. The _Writer type was added in python/typeshed#3581

Edit 2: 03ee641 fixed the issue below.

mypy.....................................................................Failed
- hook id: mypy
- exit code: 1

src/pip/_internal/operations/install/wheel.py:603: error: Argument 1 to
"writer" has incompatible type "NamedTemporaryFileResult"; expected "_Writer"
            writer = csv.writer(record_file)
                                ^
src/pip/_internal/operations/install/wheel.py:603: note: Following member(s) of "NamedTemporaryFileResult" have conflicts:
src/pip/_internal/operations/install/wheel.py:603: note:     Expected:
src/pip/_internal/operations/install/wheel.py:603: note:         def write(self, s: str) -> Any
src/pip/_internal/operations/install/wheel.py:603: note:     Got:
src/pip/_internal/operations/install/wheel.py:603: note:         @overload
src/pip/_internal/operations/install/wheel.py:603: note:         def write(self, s: bytearray) -> int
src/pip/_internal/operations/install/wheel.py:603: note:         @overload
src/pip/_internal/operations/install/wheel.py:603: note:         def write(self, s: bytes) -> int
Found 1 error in 1 file (checked 135 source files)

@deveshks deveshks changed the title Remove --pretty option from mypy Update mypy to v0.770 May 14, 2020
@deveshks deveshks force-pushed the remove-pretty-arg-from-mypy branch from eadc853 to 03ee641 Compare May 14, 2020 06:36
@deveshks
Copy link
Contributor Author

There has been changes to the PR in order to support mypy v0.770, so the previous approval no longer holds true. Hence I am going ahead and re-requesting an review.

@deveshks deveshks requested a review from uranusjr May 14, 2020 10:25
@uranusjr
Copy link
Member

uranusjr commented May 14, 2020

mypy.....................................................................Failed
- hook id: mypy
- exit code: 1

src/pip/_internal/operations/install/wheel.py:603: error: Argument 1 to
"writer" has incompatible type "NamedTemporaryFileResult"; expected "_Writer"
            writer = csv.writer(record_file)
                                ^
src/pip/_internal/operations/install/wheel.py:603: note: Following member(s) of "NamedTemporaryFileResult" have conflicts:
src/pip/_internal/operations/install/wheel.py:603: note:     Expected:
src/pip/_internal/operations/install/wheel.py:603: note:         def write(self, s: str) -> Any
src/pip/_internal/operations/install/wheel.py:603: note:     Got:
src/pip/_internal/operations/install/wheel.py:603: note:         @overload
src/pip/_internal/operations/install/wheel.py:603: note:         def write(self, s: bytearray) -> int
src/pip/_internal/operations/install/wheel.py:603: note:         @overload
src/pip/_internal/operations/install/wheel.py:603: note:         def write(self, s: bytes) -> int
Found 1 error in 1 file (checked 135 source files)

I think this is a call-site problem; we should add a cast on record_file to promise Mypy we’re passing in a bytes IO, instead of forcing NamedTemporaryFileResult.write() to pretend to accept text (because it does not).

@pradyunsg pradyunsg added skip news Does not need a NEWS file entry (eg: trivial changes) type: maintenance Related to Development and Maintenance Processes labels May 14, 2020
@deveshks
Copy link
Contributor Author

deveshks commented May 14, 2020

I think this is a call-site problem; we should add a cast on record_file to promise Mypy we’re passing in a bytes IO, instead of forcing NamedTemporaryFileResult.write() to pretend to accept text (because it does not).

As per your suggestion, I tried record_file = cast(io.BytesIO, record_file) but then mypy fails with

src/pip/_internal/operations/install/wheel.py:608: error: Argument 1 to "writer" has incompatible type "BytesIO"; expected "_Writer"
src/pip/_internal/operations/install/wheel.py:608: note: Following member(s) of "BytesIO" have conflicts:
src/pip/_internal/operations/install/wheel.py:608: note:     Expected:
src/pip/_internal/operations/install/wheel.py:608: note:         def write(self, s: str) -> Any
src/pip/_internal/operations/install/wheel.py:608: note:     Got:
src/pip/_internal/operations/install/wheel.py:608: note:         def write(self, Union[bytes, bytearray]) -> int

I also observed that as per the following function

def csv_io_kwargs(mode):
# type: (str) -> Dict[str, Any]
"""Return keyword arguments to properly open a CSV file
in the given mode.
"""
if sys.version_info.major < 3:
return {'mode': '{}b'.format(mode)}
else:
return {'mode': mode, 'newline': ''}

we open the file in string mode for Python 3, and mypy complains when we use BytesIO when running python 3.

I think that casting record_file to io.StringIO might be a better idea when using mypy with python3, something like:

if PY3:
    record_file = cast(io.StringIO, record_file)

Please let me know if there is a gap in my understanding here.

@deveshks deveshks force-pushed the remove-pretty-arg-from-mypy branch 2 times, most recently from 9a12a49 to bd07bfb Compare May 14, 2020 12:11
@deveshks deveshks requested a review from uranusjr May 14, 2020 12:37
@uranusjr
Copy link
Member

if PY3: cast(...) looks very wrong to me here, we should probably invent some kind of universal cast() call that works for all versions… I need to think about this.

@deveshks
Copy link
Contributor Author

if PY3: cast(...) looks very wrong to me here, we should probably invent some kind of universal cast() call that works for all versions… I need to think about this.

Could you tell me what should I be considering conceptually when trying to write the universal cast?

I actually added that if PY3: cast(...) since mypy was only complaining in Python 3.

I was under the impression that this is because of

def csv_io_kwargs(mode):
# type: (str) -> Dict[str, Any]
"""Return keyword arguments to properly open a CSV file
in the given mode.
"""
if sys.version_info.major < 3:
return {'mode': '{}b'.format(mode)}
else:
return {'mode': mode, 'newline': ''}

but I think that's the wrong assumption

IIUC, mypy only complains in PY3 and not PY2 because of

class NamedTemporaryFileResult(BinaryIO):

because when I change it to class NamedTemporaryFileResult(BinaryIO, StringIO):, the mypy assertions for both python versions work without cast. I think that might not be an acceptable solution as well though 😞 .

@deveshks
Copy link
Contributor Author

deveshks commented May 15, 2020

On thinking about this further, I think this might be a bug in typeshed, I have filed python/typeshed#3997 for it.

If that is a valid bug, I can go ahead and add #type: ignore on the affected line and note the typeshed issue and comment, removing a need of cast

@pradyunsg
Copy link
Member

What's the reveal_type(record_file) on Py2 vs Py3, read vs write? I have a feeling that we're hitting something interesting here.

@deveshks
Copy link
Contributor Author

I went ahead and committed the second option from the above comment, adding # noqa: F401 to the import typing step and doing

record_file_obj = cast('typing.IO[Any]', record_file)
writer = csv.writer(record_file_obj)

The first options causes tests to fail with NameError: name 'IO' is not defined

@deveshks deveshks force-pushed the remove-pretty-arg-from-mypy branch from 2e2c3ad to 09b52f2 Compare May 17, 2020 06:40
@uranusjr
Copy link
Member

Huh, surprising that typing.BinaryIO is not a subclass to typing.IO[Binary] (which would satisfy typing.IO[Any]). I guess a cast is the best approach here.

This seems to work for me (based on your branch):

diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index 142a2f5e..ab2f5779 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -38,9 +38,8 @@ from pip._internal.utils.wheel import parse_wheel
 
 if MYPY_CHECK_RUNNING:
     from email.message import Message
-    import typing  # noqa F401
     from typing import (
-        Dict, List, Optional, Sequence, Tuple, Any,
+        Dict, IO, List, Optional, Sequence, Tuple, Any,
         Iterable, Iterator, Callable, Set,
     )
 
@@ -606,7 +605,7 @@ def install_unpacked_wheel(
         # Python 3 (typing.IO[Any]) and Python 2 (typing.BinaryIO),
         # leading us to explicitly cast to typing.IO[Any] as a workaround
         # for bad Python 2 behaviour
-        record_file_obj = cast('typing.IO[Any]', record_file)
+        record_file_obj = cast('IO[Any]', record_file)
 
         writer = csv.writer(record_file_obj)
         writer.writerows(sorted_outrows(rows))  # sort to simplify testing

@deveshks
Copy link
Contributor Author

deveshks commented May 17, 2020

Huh, surprising that typing.BinaryIO is not a subclass to typing.IO[Binary] (which would satisfy typing.IO[Any]). I guess a cast is the best approach here.

This seems to work for me (based on your branch):

I actually did something similar to the patch you provided, but then flake8 complains with

src/pip/_internal/operations/install/wheel.py:41:5: F401 'typing.IO' imported but unused

Hence I went with import typing and added a #noqa in front. Is there a way to silence that warning apart from using a #noqa ?

@uranusjr
Copy link
Member

What version of pyflakes and flake8 are you using? This can happen with older versions that don’t understand the type comments. See PyCQA/pyflakes#447.

@deveshks
Copy link
Contributor Author

deveshks commented May 17, 2020

What version of pyflakes and flake8 are you using? This can happen with older versions that don’t understand the type comments. See PyCQA/pyflakes#447

The F 401: imported but unused warning actually comes when I run tox -e lint.

I now see that we are using flake8 v3.7.9 in .pre-commit-config.yaml which was released in Oct 2019

- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9

and the fix for the issue tagged above PyCQA/pyflakes#479 got in Feb 2020.

I also tried the the latest version of flake8, v3.8.1, but that also raises the same warning after applying your changes as below.

$ git diff src/pip/_internal/operations/install/wheel.py 
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index 142a2f5e..ab2f5779 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -38,9 +38,8 @@ from pip._internal.utils.wheel import parse_wheel
 
 if MYPY_CHECK_RUNNING:
     from email.message import Message
-    import typing  # noqa F401
     from typing import (
-        Dict, List, Optional, Sequence, Tuple, Any,
+        Dict, IO, List, Optional, Sequence, Tuple, Any,
         Iterable, Iterator, Callable, Set,
     )
 
@@ -606,7 +605,7 @@ def install_unpacked_wheel(
         # Python 3 (typing.IO[Any]) and Python 2 (typing.BinaryIO),
         # leading us to explicitly cast to typing.IO[Any] as a workaround
         # for bad Python 2 behaviour
-        record_file_obj = cast('typing.IO[Any]', record_file)
+        record_file_obj = cast('IO[Any]', record_file)
 
         writer = csv.writer(record_file_obj)
         writer.writerows(sorted_outrows(rows))  # sort to simplify testing
$ flake8 --version
3.8.1 (mccabe: 0.6.1, pycodestyle: 2.6.0, pyflakes: 2.2.0) CPython 3.8.2 on Darwin

$ flake8 src/pip/_internal/operations/install/wheel.py 
src/pip/_internal/operations/install/wheel.py:41:5: F401 'typing.IO' imported but unused

Same thing with v2.2.0 of pyflakes

$ pyflakes --version
2.2.0 Python 3.8.2 on Darwin

$ pyflakes src/pip/_internal/operations/install/wheel.py
src/pip/_internal/operations/install/wheel.py:41:5 'typing.IO' imported but unused

@deveshks
Copy link
Contributor Author

Okay, so if I use typing.cast instead of pip._internal.utils.typing.cast, the latest flake8 stops complaining.

$ git diff src/pip/_internal/operations/install/wheel.py
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index 142a2f5e..120c48a4 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -32,16 +32,15 @@ from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
 from pip._internal.utils.filesystem import adjacent_tmp_file, replace
 from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file
 from pip._internal.utils.temp_dir import TempDirectory
-from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast
+from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 from pip._internal.utils.unpacking import current_umask, unpack_file
 from pip._internal.utils.wheel import parse_wheel
 
 if MYPY_CHECK_RUNNING:
     from email.message import Message
-    import typing  # noqa F401
     from typing import (
-        Dict, List, Optional, Sequence, Tuple, Any,
-        Iterable, Iterator, Callable, Set,
+        Dict, IO, List, Optional, Sequence, Tuple, Any,
+        Iterable, Iterator, Callable, Set, cast
     )
 
     from pip._internal.models.scheme import Scheme
@@ -606,7 +605,7 @@ def install_unpacked_wheel(
         # Python 3 (typing.IO[Any]) and Python 2 (typing.BinaryIO),
         # leading us to explicitly cast to typing.IO[Any] as a workaround
         # for bad Python 2 behaviour
-        record_file_obj = cast('typing.IO[Any]', record_file)
+        record_file_obj = cast('IO[Any]', record_file)
 
         writer = csv.writer(record_file_obj)
         writer.writerows(sorted_outrows(rows))  # sort to simplify testing
$ flake8 src/pip/_internal/operations/install/wheel.py

But then the functional tests associated to wheel.py, e.g. tests/functional/test_install_wheel.py start failing with NameError: name 'cast' is not defined.

For e.g.

Failing test_install_wheel.py::test_install_from_future_wheel_version
$ tox -e py tests/functional/test_install_wheel.py::test_install_from_future_wheel_version
GLOB sdist-make: /Users/devesh/pip/setup.py
py inst-nodeps: /Users/devesh/pip/.tox/.tmp/package/1/pip-20.2.dev0.zip
py installed: apipkg==1.5,atomicwrites==1.4.0,attrs==19.3.0,cffi==1.14.0,coverage==5.1,cryptography==2.8,csv23==0.3.1,execnet==1.7.1,freezegun==0.3.15,mock==4.0.2,more-itertools==8.3.0,pip @ file:///Users/devesh/pip/.tox/.tmp/package/1/pip-20.2.dev0.zip,pluggy==0.13.1,pretend==1.0.9,py==1.8.1,pycparser==2.20,pytest==3.8.2,pytest-cov==2.8.1,pytest-forked==1.1.3,pytest-rerunfailures==6.0,pytest-timeout==1.3.4,pytest-xdist==1.27.0,python-dateutil==2.8.1,PyYAML==5.3.1,scripttest==1.3,setuptools==46.1.3,six==1.14.0,virtualenv @ https://github.com/pypa/virtualenv/archive/legacy.zip,Werkzeug==0.16.0,wheel==0.34.2
py run-test-pre: PYTHONHASHSEED='3686262331'
py run-test-pre: commands[0] | python -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' /Users/devesh/pip/tests/data/common_wheels
py run-test-pre: commands[1] | python /Users/devesh/pip/tools/tox_pip.py wheel -w /Users/devesh/pip/tests/data/common_wheels -r /Users/devesh/pip/tools/requirements/tests-common_wheels.txt
Collecting setuptools>=40.8.0
  Using cached setuptools-46.4.0-py3-none-any.whl (583 kB)
  Saved ./tests/data/common_wheels/setuptools-46.4.0-py3-none-any.whl
Collecting wheel
  Using cached wheel-0.34.2-py2.py3-none-any.whl (26 kB)
  Saved ./tests/data/common_wheels/wheel-0.34.2-py2.py3-none-any.whl
Skipping setuptools, due to already being wheel.
Skipping wheel, due to already being wheel.
py run-test: commands[0] | pytest --timeout 300 tests/functional/test_install_wheel.py::test_install_from_future_wheel_version
=========================================================================================================== test session starts ============================================================================================================
platform darwin -- Python 3.8.2, pytest-3.8.2, py-1.8.1, pluggy-0.13.1
rootdir: /Users/devesh/pip, inifile: setup.cfg
plugins: timeout-1.3.4, rerunfailures-6.0, forked-1.1.3, cov-2.8.1, xdist-1.27.0
timeout: 300.0s
timeout method: signal
timeout func_only: False
collected 1 item                                                                                                                                                                                                                           

tests/functional/test_install_wheel.py F                                                                                                                                                                                             [100%]

================================================================================================================= FAILURES =================================================================================================================
__________________________________________________________________________________________________ test_install_from_future_wheel_version __________________________________________________________________________________________________

script = <tests.lib.PipTestEnvironment object at 0x105091c10>, tmpdir = Path('/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/test_install_from_future_wheel0')

    def test_install_from_future_wheel_version(script, tmpdir):
        """
        Test installing a wheel with a WHEEL metadata version that is:
        - a major version ahead of what we expect (not ok), and
        - a minor version ahead of what we expect (ok)
        """
        from tests.lib import TestFailure
        package = make_wheel_with_file(
            name="futurewheel",
            version="3.0",
            wheel_metadata_updates={"Wheel-Version": "3.0"},
        ).save_to_dir(tmpdir)
    
        result = script.pip('install', package, '--no-index', expect_error=True)
        with pytest.raises(TestFailure):
            result.assert_installed('futurewheel', without_egg_link=True,
                                    editable=False)
    
        package = make_wheel_with_file(
            name="futurewheel",
            version="1.9",
            wheel_metadata_updates={"Wheel-Version": "1.9"},
        ).save_to_dir(tmpdir)
>       result = script.pip(
            'install', package, '--no-index', expect_stderr=True
        )

tests/functional/test_install_wheel.py:43: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <tests.lib.PipTestEnvironment object at 0x105091c10>
args = ('python', '-m', 'pip', 'install', '/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/test_install_from_future_wheel0/futurewheel-1.9-py2.py3-none-any.whl', '--no-index')
kw = {'expect_stderr': True}, cwd = Path('/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/test_install_from_future_wheel0/workspace/scratch'), run_from = None, allow_stderr_error = False
allow_stderr_warning = True, allow_error = None, expect_error = None

    def run(self, *args, **kw):
        """
            :param allow_stderr_error: whether a logged error is allowed in
                stderr.  Passing True for this argument implies
                `allow_stderr_warning` since warnings are weaker than errors.
            :param allow_stderr_warning: whether a logged warning (or
                deprecation message) is allowed in stderr.
            :param allow_error: if True (default is False) does not raise
                exception when the command exit value is non-zero.  Implies
                expect_error, but in contrast to expect_error will not assert
                that the exit value is zero.
            :param expect_error: if False (the default), asserts that the command
                exits with 0.  Otherwise, asserts that the command exits with a
                non-zero exit code.  Passing True also implies allow_stderr_error
                and allow_stderr_warning.
            :param expect_stderr: whether to allow warnings in stderr (equivalent
                to `allow_stderr_warning`).  This argument is an abbreviated
                version of `allow_stderr_warning` and is also kept for backwards
                compatibility.
            """
        if self.verbose:
            print('>> running {args} {kw}'.format(**locals()))
    
        cwd = kw.pop('cwd', None)
        run_from = kw.pop('run_from', None)
        assert not cwd or not run_from, "Don't use run_from; it's going away"
        cwd = cwd or run_from or self.cwd
        if sys.platform == 'win32':
            # Partial fix for ScriptTest.run using `shell=True` on Windows.
            args = [str(a).replace('^', '^^').replace('&', '^&') for a in args]
    
        # Remove `allow_stderr_error`, `allow_stderr_warning` and
        # `allow_error` before calling run() because PipTestEnvironment
        # doesn't support them.
        allow_stderr_error = kw.pop('allow_stderr_error', None)
        allow_stderr_warning = kw.pop('allow_stderr_warning', None)
        allow_error = kw.pop('allow_error', None)
        if allow_error:
            kw['expect_error'] = True
    
        # Propagate default values.
        expect_error = kw.get('expect_error')
        if expect_error:
            # Then default to allowing logged errors.
            if allow_stderr_error is not None and not allow_stderr_error:
                raise RuntimeError(
                    'cannot pass allow_stderr_error=False with '
                    'expect_error=True'
                )
            allow_stderr_error = True
    
        elif kw.get('expect_stderr'):
            # Then default to allowing logged warnings.
            if allow_stderr_warning is not None and not allow_stderr_warning:
                raise RuntimeError(
                    'cannot pass allow_stderr_warning=False with '
                    'expect_stderr=True'
                )
            allow_stderr_warning = True
    
        if allow_stderr_error:
            if allow_stderr_warning is not None and not allow_stderr_warning:
                raise RuntimeError(
                    'cannot pass allow_stderr_warning=False with '
                    'allow_stderr_error=True'
                )
    
        # Default values if not set.
        if allow_stderr_error is None:
            allow_stderr_error = False
        if allow_stderr_warning is None:
            allow_stderr_warning = allow_stderr_error
    
        # Pass expect_stderr=True to allow any stderr.  We do this because
        # we do our checking of stderr further on in check_stderr().
        kw['expect_stderr'] = True
>       result = super(PipTestEnvironment, self).run(cwd=cwd, *args, **kw)
E       AssertionError: Script returned code: 2

tests/lib/__init__.py:605: AssertionError
---------------------------------------------------------------------------------------------------------- Captured stderr setup -----------------------------------------------------------------------------------------------------------
warning: no files found matching 'docs/docutils.conf'
warning: no previously-included files found matching '.coveragerc'
warning: no previously-included files found matching '.mailmap'
warning: no previously-included files found matching '.appveyor.yml'
warning: no previously-included files found matching '.travis.yml'
warning: no previously-included files found matching '.readthedocs.yml'
warning: no previously-included files found matching '.pre-commit-config.yaml'
warning: no previously-included files found matching 'tox.ini'
warning: no previously-included files found matching 'noxfile.py'
warning: no files found matching 'Makefile' under directory 'docs'
warning: no files found matching '*.rst' under directory 'docs'
warning: no files found matching '*.py' under directory 'docs'
warning: no files found matching '*.bat' under directory 'docs'
warning: no previously-included files found matching 'src/pip/_vendor/six'
warning: no previously-included files found matching 'src/pip/_vendor/six/moves'
warning: no previously-included files matching '*.pyi' found under directory 'src/pip/_vendor'
no previously-included directories found matching '.github'
no previously-included directories found matching '.azure-pipelines'
no previously-included directories found matching 'docs/build'
no previously-included directories found matching 'news'
no previously-included directories found matching 'tasks'
no previously-included directories found matching 'tests'
no previously-included directories found matching 'tools'
----------------------------------------------------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------------------------------------------------
Script result: python -m pip install /private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/test_install_from_future_wheel0/futurewheel-1.9-py2.py3-none-any.whl --no-index
  return code: 2
-- stderr: --------------------
WARNING: Installing from a newer Wheel-Version (1.9)
WARNING: Installing from a newer Wheel-Version (1.9)
  WARNING: Installing from a newer Wheel-Version (1.9)
ERROR: Exception:
Traceback (most recent call last):
  File "/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/pip0/pip/src/pip/_internal/cli/base_command.py", line 188, in _main
    status = self.run(options, args)
  File "/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/pip0/pip/src/pip/_internal/cli/req_command.py", line 184, in wrapper
    return func(self, options, args)
  File "/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/pip0/pip/src/pip/_internal/commands/install.py", line 395, in run
    installed = install_given_reqs(
  File "/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/pip0/pip/src/pip/_internal/req/__init__.py", line 67, in install_given_reqs
    requirement.install(
  File "/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/pip0/pip/src/pip/_internal/req/req_install.py", line 804, in install
    install_wheel(
  File "/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/pip0/pip/src/pip/_internal/operations/install/wheel.py", line 629, in install_wheel
    install_unpacked_wheel(
  File "/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/pip0/pip/src/pip/_internal/operations/install/wheel.py", line 608, in install_unpacked_wheel
    record_file_obj = cast('IO[Any]', record_file)
NameError: name 'cast' is not defined

-- stdout: --------------------
Processing /private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/test_install_from_future_wheel0/futurewheel-1.9-py2.py3-none-any.whl
Installing collected packages: futurewheel

-- created: -------------------
  venv/lib/python3.8/site-packages/futurewheel
                  futurewheel-1.9.dist-info
                      INSTALLER  (4 bytes)
                      METADATA  (54 bytes)
                      RECORD  (292 bytes)
                      RECORDo2fgi27t.tmp  (0 bytes)
                      WHEEL  (104 bytes)
                      direct_url.json  (191 bytes)
                  futurewheel/__init__.py  (9 bytes)
-- updated: -------------------
  venv/lib/python3.8/site-packages
========================================================================================================= short test summary info ==========================================================================================================
FAIL tests/functional/test_install_wheel.py::test_install_from_future_wheel_version
============================================================================================================= warnings summary =============================================================================================================
/private/var/folders/xg/blp845_s0xn093dyrtgy936h0000gp/T/pytest-of-devesh/pytest-190/pip0/pip/src/pip/_vendor/toml/decoder.py:47: DeprecationWarning: invalid escape sequence \.

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================================================================================================== 1 failed, 1 warnings in 4.87 seconds ===================================================================================================
ERROR: InvocationError for command /Users/devesh/pip/.tox/py/bin/pytest --timeout 300 tests/functional/test_install_wheel.py::test_install_from_future_wheel_version (exited with code 1)
_________________________________________________________________________________________________________________ summary __________________________________________________________________________________________________________________
ERROR:   py: commands failed

@pradyunsg
Copy link
Member

record_file_obj = cast(IO[Any], record_file)

This needs us to import from typing at runtime, which we can't do (thanks Python 2!). :(


Hey @asottile! o/

Any suggestions on how to avoid this situation we're in, where we're trying to get our pip._internal.utils.typing module to serve as the "source" for the cast function, but want it to be treated like typing.cast?

@pradyunsg
Copy link
Member

But then the functional tests associated to wheel.py, e.g. tests/functional/test_install_wheel.py start failing with NameError: name 'cast' is not defined.

Yea... MYPY_CHECK_RUNNING is always False at runtime, but mypy does consider the body of that block during type analysis (by design).

The main "trick" in our don't-import-typing-at-runtime approach, is that all from typing ... statements are inside if False blocks (or equivalents), that mypy does look at but don't run at runtime.

@asottile
Copy link
Contributor

record_file_obj = cast(IO[Any], record_file)

This needs us to import from typing at runtime, which we can't do (thanks Python 2!). :(

Hey @asottile! o/

Any suggestions on how to avoid this situation we're in, where we're trying to get our pip._internal.utils.typing module to serve as the "source" for the cast function, but want it to be treated like typing.cast?

I just tried it locally, seems to be working already?

@deveshks
Copy link
Contributor Author

I just tried it locally, seems to be working already?

May I know what you tried locally? Did you check out the PR locally and ran tox -e lint? That should pass because I have a #noqa 401 at the offending line.

@asottile
Copy link
Contributor

I just tried it locally, seems to be working already?

May I know what you tried locally? Did you check out the PR locally and ran tox -e lint? That should pass because I have a #noqa 401 at the offending line.

oh, I was just running pre-commit run mypy --all-files, is there something else I should be trying?

@deveshks
Copy link
Contributor Author

deveshks commented May 17, 2020

oh, I was just running pre-commit run mypy --all-files, is there something else I should be trying?

So we have pip._internal.utils.typing where we use typing.cast at the time of mypy check, and we use the cast in the else block at runtime.

The cast in the else block is the cast we are using in record_file_obj = cast(IO[Any], record_file) at runtime, but we want it to treated as typing.cast when running using flake8.

The following minimal example should be able to help you understand the problem better.

import io

# pip/src/pip/_internal/utils/typing.py repro
MYPY_CHECK_RUNNING = False

if MYPY_CHECK_RUNNING:
    from typing import cast, IO, Any
else:
    # typing's cast() is needed at runtime, but we don't want to import typing.
    # Thus, we use a dummy no-op version, which we tell mypy to ignore.
    def cast(_, value):  # type: ignore
        return value

# pip/src/pip/_internal/operations/install/wheel.py repro
with io.BytesIO() as record_file:
    record_file_obj = cast('IO[Any]', record_file)

Running flake8 on the following raises the warnings

/Users/devesh/Library/Preferences/PyCharmCE2019.3/scratches/scratch.py:7:5: F401 'typing.IO' imported but unused
/Users/devesh/Library/Preferences/PyCharmCE2019.3/scratches/scratch.py:7:5: F401 'typing.Any' imported but unused

@asottile
Copy link
Contributor

pyflakes only has special handling of typing.cast (it does not follow imports) and only in pyflakes 2.2+ (pip is currently using old flake8 and therefore old pyflakes)

this patch passes without noqa:

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3e2c0a2a..0a7847c1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -17,7 +17,7 @@ repos:
     exclude: .patch
 
 - repo: https://gitlab.com/pycqa/flake8
-  rev: 3.7.9
+  rev: 3.8.1
   hooks:
   - id: flake8
     exclude: tests/data
diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py
index a61294ba..3e09e41f 100644
--- a/src/pip/_internal/commands/show.py
+++ b/src/pip/_internal/commands/show.py
@@ -93,7 +93,7 @@ def search_packages_info(query):
             # RECORDs should be part of .dist-info metadatas
             if dist.has_metadata('RECORD'):
                 lines = dist.get_metadata_lines('RECORD')
-                paths = [l.split(',')[0] for l in lines]
+                paths = [line.split(',')[0] for line in lines]
                 paths = [os.path.join(dist.location, p) for p in paths]
                 file_list = [os.path.relpath(p, dist.location) for p in paths]
 
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index 142a2f5e..a548553f 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -32,15 +32,16 @@ from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
 from pip._internal.utils.filesystem import adjacent_tmp_file, replace
 from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file
 from pip._internal.utils.temp_dir import TempDirectory
-from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast
+from pip._internal.utils.typing import MYPY_CHECK_RUNNING
 from pip._internal.utils.unpacking import current_umask, unpack_file
 from pip._internal.utils.wheel import parse_wheel
 
-if MYPY_CHECK_RUNNING:
+if not MYPY_CHECK_RUNNING:
+    from pip._internal.utils.typing import cast
+else:
     from email.message import Message
-    import typing  # noqa F401
     from typing import (
-        Dict, List, Optional, Sequence, Tuple, Any,
+        cast, Dict, IO, List, Optional, Sequence, Tuple, Any,
         Iterable, Iterator, Callable, Set,
     )
 
@@ -606,7 +607,7 @@ def install_unpacked_wheel(
         # Python 3 (typing.IO[Any]) and Python 2 (typing.BinaryIO),
         # leading us to explicitly cast to typing.IO[Any] as a workaround
         # for bad Python 2 behaviour
-        record_file_obj = cast('typing.IO[Any]', record_file)
+        record_file_obj = cast('IO[Any]', record_file)
 
         writer = csv.writer(record_file_obj)
         writer.writerows(sorted_outrows(rows))  # sort to simplify testing
diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py
index 09031825..65cd3888 100644
--- a/src/pip/_internal/utils/misc.py
+++ b/src/pip/_internal/utils/misc.py
@@ -541,7 +541,7 @@ class FakeFile(object):
     """Wrap a list of lines in an object with readline() to make
     ConfigParser happy."""
     def __init__(self, lines):
-        self._gen = (l for l in lines)
+        self._gen = iter(lines)
 
     def readline(self):
         try:
diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py
index fcaeeb6c..261380df 100644
--- a/src/pip/_internal/wheel_builder.py
+++ b/src/pip/_internal/wheel_builder.py
@@ -36,7 +36,9 @@ logger = logging.getLogger(__name__)
 
 
 def _contains_egg_info(
-        s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)):
+        s,
+        _egg_info_re=re.compile(
+            r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.IGNORECASE)):
     # type: (str, Pattern[str]) -> bool
     """Determine whether the string looks like an egg_info.
 
diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py
index 0c00060a..40b0c3c7 100644
--- a/tests/functional/test_install_reqs.py
+++ b/tests/functional/test_install_reqs.py
@@ -420,7 +420,7 @@ def test_install_with_extras_from_install(script, data):
     )
     result = script.pip_install_local(
         '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]')
-    assert script.site_packages / 'singlemodule.py'in result.files_created
+    assert script.site_packages / 'singlemodule.py' in result.files_created
 
 
 def test_install_with_extras_joined(script, data):
@@ -432,7 +432,7 @@ def test_install_with_extras_joined(script, data):
         '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]'
     )
     assert script.site_packages / 'simple' in result.files_created
-    assert script.site_packages / 'singlemodule.py'in result.files_created
+    assert script.site_packages / 'singlemodule.py' in result.files_created
 
 
 def test_install_with_extras_editable_joined(script, data):
@@ -443,7 +443,7 @@ def test_install_with_extras_editable_joined(script, data):
     result = script.pip_install_local(
         '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]')
     assert script.site_packages / 'simple' in result.files_created
-    assert script.site_packages / 'singlemodule.py'in result.files_created
+    assert script.site_packages / 'singlemodule.py' in result.files_created
 
 
 def test_install_distribution_full_union(script, data):

I had to fix a bunch of stuff and work around PyCQA/pycodestyle#935

@deveshks
Copy link
Contributor Author

deveshks commented May 17, 2020

Thanks @asottile for the diff. Upgrading to flake 3.8.1 and using the if-else logic works perfectly by using the correct cast function, but I would defer to @pradyunsg to let me know if we want to upgrade to latest flake-8 and then use the if-else logic.

I will probably upgrade flake in a separate PR as well after fixing the issues with other files as per your diff.

@pradyunsg
Copy link
Member

Wheee! That if-else idea is smart @asottile! Thanks much for your help here! ^>^

@deveshks It's probably cleaner to upgrade flake8+pyflakes separately. If you wanna do it in this PR, that works too!

@deveshks
Copy link
Contributor Author

deveshks commented May 17, 2020

@deveshks It's probably cleaner to upgrade flake8+pyflakes separately. If you wanna do it in this PR, that works too!

I completely agree. What I am thinking is that

  1. We go ahead and approve/merge this PR as is for now.

  2. I create a follow up PR from the master, and then apply the patch provided by @asottile which will both upgrade flake8 as well as remove the superfluos #noqa as part of the code changes related to upgrade.

Or If merging is only allowed for changes which don't affect the pip codebase a lot, I will make the flake8 changes here, and then this PR can be merged when the master opens for regular changes post 20.1.1 again.

Let me know if you think is the best option.

@pradyunsg
Copy link
Member

the master opens for regular changes post 20.1.1 again.

master branch is open; there's no reason to avoid-merging-PRs prior to a bugfix release anyway, since that will be based off the last tag, and not the master branch.

Let me know if you think is the best option.

Merge now, and improve in a follow up sounds good to me! :)

@deveshks
Copy link
Contributor Author

deveshks commented May 17, 2020

Merge now, and improve in a follow up sounds good to me! :)

Thanks, I will create the new PR accordingly then from the master once this is merged.

@deveshks deveshks force-pushed the remove-pretty-arg-from-mypy branch 3 times, most recently from 3b68f47 to 4d208b0 Compare May 18, 2020 06:56
@pradyunsg pradyunsg merged commit d0ab9bd into pypa:master May 18, 2020
@deveshks deveshks deleted the remove-pretty-arg-from-mypy branch May 18, 2020 14:11
@deveshks
Copy link
Contributor Author

Thanks, I will create the new PR accordingly then from the master once this is merged.

#8261 has been added to upgrade flake8.

@pradyunsg pradyunsg removed the request for review from uranusjr May 19, 2020 05:53
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 14, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
skip news Does not need a NEWS file entry (eg: trivial changes) type: maintenance Related to Development and Maintenance Processes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants