From 29fdba381ec951d63320be6975ce5552e44def64 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Tue, 26 Mar 2019 01:43:17 -0700 Subject: [PATCH 1/4] Drop support for running with 3.4 --- .travis.yml | 3 --- README.md | 2 +- docs/source/common_issues.rst | 2 +- docs/source/getting_started.rst | 2 +- mypy/dmypy_server.py | 2 +- mypy/main.py | 6 ++--- mypy/test/helpers.py | 18 ------------- mypy/test/testpep561.py | 23 ++++++++-------- mypy/test/testpythoneval.py | 8 +++--- mypy/util.py | 47 --------------------------------- runtests.py | 2 +- setup.py | 6 ++--- 12 files changed, 27 insertions(+), 94 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1a95050cd5be..227213fdebc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,6 @@ env: jobs: include: - - name: "run test suite with python 3.4" - python: 3.4 - dist: trusty # Specifically request 3.5.1 because we need to be compatible with that. - name: "run test suite with python 3.5.1" python: 3.5.1 diff --git a/README.md b/README.md index eac41ddbef50..758a458fee92 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ See 'Development status' below. Requirements ------------ -You need Python 3.4 or later to run mypy. You can have multiple Python +You need Python 3.5 or later to run mypy. You can have multiple Python versions (2.x and 3.x) installed on the same system without problems. In Ubuntu, Mint and Debian you can install Python 3 like this: diff --git a/docs/source/common_issues.rst b/docs/source/common_issues.rst index 1c73f7ae4347..a4da8ec13546 100644 --- a/docs/source/common_issues.rst +++ b/docs/source/common_issues.rst @@ -14,7 +14,7 @@ Can't install mypy using pip If installation fails, you've probably hit one of these issues: -* Mypy needs Python 3.4 or later to run. +* Mypy needs Python 3.5 or later to run. * You may have to run pip like this: ``python3 -m pip install mypy``. diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index b5157101568c..fd206491f88a 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -12,7 +12,7 @@ may not make much sense otherwise. Installing and running mypy *************************** -Mypy requires Python 3.4 or later to run. Once you've +Mypy requires Python 3.5 or later to run. Once you've `installed Python 3 `_, install mypy using pip: diff --git a/mypy/dmypy_server.py b/mypy/dmypy_server.py index 79e5769fee2e..3b464a80afab 100644 --- a/mypy/dmypy_server.py +++ b/mypy/dmypy_server.py @@ -14,6 +14,7 @@ import sys import time import traceback +from contextlib import redirect_stderr, redirect_stdout from typing import AbstractSet, Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple @@ -30,7 +31,6 @@ from mypy.options import Options from mypy.suggestions import SuggestionFailure, SuggestionEngine from mypy.typestate import reset_global_state -from mypy.util import redirect_stderr, redirect_stdout from mypy.version import __version__ diff --git a/mypy/main.py b/mypy/main.py index ab23f90c7fde..325a9c91608d 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -54,9 +54,9 @@ def main(script_path: Optional[str], args: Optional[List[str]] = None) -> None: be used. """ # Check for known bad Python versions. - if sys.version_info[:2] < (3, 4): - sys.exit("Running mypy with Python 3.3 or lower is not supported; " - "please upgrade to 3.4 or newer") + if sys.version_info[:2] < (3, 5): + sys.exit("Running mypy with Python 3.4 or lower is not supported; " + "please upgrade to 3.5 or newer") if sys.version_info[:3] == (3, 5, 0): sys.exit("Running mypy with Python 3.5.0 is not supported; " "please upgrade to 3.5.1 or newer") diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py index f23e3d739735..7a3ea0a831da 100644 --- a/mypy/test/helpers.py +++ b/mypy/test/helpers.py @@ -397,24 +397,6 @@ def split_lines(*streams: bytes) -> List[str]: ] -def run_command(cmdline: List[str], *, env: Optional[Dict[str, str]] = None, - timeout: int = 300, cwd: str = test_temp_dir) -> Tuple[int, List[str]]: - """A poor man's subprocess.run() for 3.4 compatibility.""" - process = subprocess.Popen( - cmdline, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=cwd, - ) - try: - out, err = process.communicate(timeout=timeout) - except subprocess.TimeoutExpired: - out = err = b'' - process.kill() - return process.returncode, split_lines(out, err) - - def copy_and_fudge_mtime(source_path: str, target_path: str) -> None: # In some systems, mtime has a resolution of 1 second which can # cause annoying-to-debug issues when a file has the same size diff --git a/mypy/test/testpep561.py b/mypy/test/testpep561.py index ed0e56e55181..4b988503df79 100644 --- a/mypy/test/testpep561.py +++ b/mypy/test/testpep561.py @@ -1,6 +1,8 @@ from contextlib import contextmanager from enum import Enum import os +import subprocess +from subprocess import PIPE import sys import tempfile from typing import Tuple, List, Generator, Optional @@ -9,7 +11,6 @@ import mypy.api from mypy.modulefinder import get_site_packages_dirs from mypy.test.config import package_path -from mypy.test.helpers import run_command from mypy.util import try_find_python2_interpreter # NOTE: options.use_builtins_fixtures should not be set in these @@ -135,13 +136,13 @@ def virtualenv(self, # Sadly, we need virtualenv, as the Python 3 venv module does not support creating a venv # for Python 2, and Python 2 does not have its own venv. with tempfile.TemporaryDirectory() as venv_dir: - returncode, lines = run_command([sys.executable, - '-m', - 'virtualenv', - '-p{}'.format(python_executable), - venv_dir], cwd=os.getcwd()) - if returncode != 0: - err = '\n'.join(lines) + proc = subprocess.run([sys.executable, + '-m', + 'virtualenv', + '-p{}'.format(python_executable), + venv_dir], cwd=os.getcwd(), stdout=PIPE, stderr=PIPE) + if proc.returncode != 0: + err = proc.stdout.decode('utf-8') + proc.stderr.decode('utf-8') self.fail("Failed to create venv. Do you have virtualenv installed?\n" + err) if sys.platform == 'win32': yield venv_dir, os.path.abspath(os.path.join(venv_dir, 'Scripts', 'python')) @@ -165,9 +166,9 @@ def install_package(self, pkg: str, install_cmd.append('develop') else: install_cmd.append('install') - returncode, lines = run_command(install_cmd, cwd=working_dir) - if returncode != 0: - self.fail('\n'.join(lines)) + proc = subprocess.run(install_cmd, cwd=working_dir, stdout=PIPE, stderr=PIPE) + if proc.returncode != 0: + self.fail(proc.stdout.decode('utf-8') + proc.stderr.decode('utf-8')) def setUp(self) -> None: self.simple_prog = ExampleProg(SIMPLE_PROGRAM) diff --git a/mypy/test/testpythoneval.py b/mypy/test/testpythoneval.py index bd507e630e8b..89a3afaca110 100644 --- a/mypy/test/testpythoneval.py +++ b/mypy/test/testpythoneval.py @@ -13,6 +13,8 @@ import os import os.path import re +import subprocess +from subprocess import PIPE import sys from tempfile import TemporaryDirectory @@ -23,7 +25,7 @@ from mypy.defaults import PYTHON3_VERSION from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite -from mypy.test.helpers import assert_string_arrays_equal, run_command +from mypy.test.helpers import assert_string_arrays_equal, split_lines from mypy.util import try_find_python2_interpreter from mypy import api @@ -90,8 +92,8 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None output.append(line.rstrip("\r\n")) if returncode == 0: # Execute the program. - returncode, interp_out = run_command([interpreter, program]) - output.extend(interp_out) + proc = subprocess.run([interpreter, program], cwd=test_temp_dir, stdout=PIPE, stderr=PIPE) + output.extend(split_lines(proc.stdout, proc.stderr)) # Remove temp file. os.remove(program_path) for i, line in enumerate(output): diff --git a/mypy/util.py b/mypy/util.py index df054814df4e..3455e9c8e70b 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -257,50 +257,3 @@ def hard_exit(status: int = 0) -> None: def unmangle(name: str) -> str: """Remove internal suffixes from a short name.""" return name.rstrip("'") - - -# The following is a backport of stream redirect utilities from Lib/contextlib.py -# We need this for 3.4 support. They can be removed in March 2019! - - -class _RedirectStream: - - _stream = None # type: ClassVar[str] - - def __init__(self, new_target: TextIO) -> None: - self._new_target = new_target - # We use a list of old targets to make this CM re-entrant - self._old_targets = [] # type: List[TextIO] - - def __enter__(self) -> TextIO: - self._old_targets.append(getattr(sys, self._stream)) - setattr(sys, self._stream, self._new_target) - return self._new_target - - def __exit__(self, - exc_ty: 'Optional[Type[BaseException]]' = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional[TracebackType] = None, - ) -> bool: - setattr(sys, self._stream, self._old_targets.pop()) - return False - - -class redirect_stdout(_RedirectStream): - """Context manager for temporarily redirecting stdout to another file. - # How to send help() to stderr - with redirect_stdout(sys.stderr): - help(dir) - # How to write help() to a file - with open('help.txt', 'w') as f: - with redirect_stdout(f): - help(pow) - """ - - _stream = "stdout" - - -class redirect_stderr(_RedirectStream): - """Context manager for temporarily redirecting stderr to another file.""" - - _stream = "stderr" diff --git a/runtests.py b/runtests.py index 9a2ac05a9115..36c2dfa2a014 100755 --- a/runtests.py +++ b/runtests.py @@ -6,7 +6,7 @@ # Use the Python provided to execute the script, or fall back to a sane default -if version_info >= (3, 4, 0): +if version_info >= (3, 5, 0): python_name = executable else: if platform == 'win32': diff --git a/setup.py b/setup.py index 07c085133e23..e3dc63889861 100644 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ import os.path import sys -if sys.version_info < (3, 4, 0): - sys.stderr.write("ERROR: You need Python 3.4 or later to use mypy.\n") +if sys.version_info < (3, 5, 0): + sys.stderr.write("ERROR: You need Python 3.5 or later to use mypy.\n") exit(1) # we'll import stuff from the source tree, let's ensure is on the sys path @@ -150,7 +150,6 @@ def run(self): 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', @@ -179,7 +178,6 @@ def run(self): 'mypy_extensions >= 0.4.0, < 0.5.0', ], extras_require = { - ':python_version < "3.5"': 'typing >= 3.5.3', 'dmypy': 'psutil >= 5.4.0, < 5.5.0; sys_platform!="win32"', }, include_package_data=True, From d40728a5a2cd87d4a1fbd909f6de26d3ff1bda24 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Tue, 26 Mar 2019 10:13:04 -0700 Subject: [PATCH 2/4] Add some more changes for no longer supporting 3.3/3.4 --- mypy/build.py | 8 ++------ mypy/fastparse.py | 4 ++-- mypy/fastparse2.py | 4 ++-- tox.ini | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 9479351daac3..c6c44fd6ead8 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2961,9 +2961,7 @@ def dfs(v: str) -> Iterator[Set[str]]: for w in edges[v]: if w not in index: - # For Python >= 3.3, replace with "yield from dfs(w)" - for scc in dfs(w): - yield scc + yield from dfs(w) elif w not in identified: while index[w] < boundaries[-1]: boundaries.pop() @@ -2977,9 +2975,7 @@ def dfs(v: str) -> Iterator[Set[str]]: for v in vertices: if v not in index: - # For Python >= 3.3, replace with "yield from dfs(v)" - for scc in dfs(v): - yield scc + yield from dfs(v) def topsort(data: Dict[AbstractSet[str], diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 7d890570519b..577da287e421 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -98,7 +98,7 @@ def ast3_parse(source: Union[str, bytes], filename: str, mode: str, NamedExpr = Any Constant = Any except ImportError: - if sys.version_info.minor > 2: + if sys.version_info.minor > 4: try: from typed_ast import ast35 # type: ignore except ImportError: @@ -112,7 +112,7 @@ def ast3_parse(source: Union[str, bytes], filename: str, mode: str, file=sys.stderr) else: print('Mypy requires the typed_ast package, which is only compatible with\n' - 'Python 3.3 and greater.', file=sys.stderr) + 'Python 3.5 and greater.', file=sys.stderr) sys.exit(1) N = TypeVar('N', bound=Node) diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 016a137c50a3..40b7e6821994 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -60,7 +60,7 @@ # Import ast3 from fastparse, which has special case for Python 3.8 from mypy.fastparse import ast3, ast3_parse except ImportError: - if sys.version_info.minor > 2: + if sys.version_info.minor > 4: try: from typed_ast import ast35 # type: ignore except ImportError: @@ -74,7 +74,7 @@ file=sys.stderr) else: print('Mypy requires the typed_ast package, which is only compatible with\n' - 'Python 3.3 and greater.', file=sys.stderr) + 'Python 3.5 and greater.', file=sys.stderr) sys.exit(1) N = TypeVar('N', bound=Node) diff --git a/tox.ini b/tox.ini index fc2604cc7f77..3c5c18262b44 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 3.4.0 +minversion = 3.5.1 skip_missing_interpreters = true envlist = py34, py35, From 6fa4c271e713e6fcc196f34ada2ebf7510a665a9 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Tue, 26 Mar 2019 10:39:07 -0700 Subject: [PATCH 3/4] Check Python versions at all entry points --- mypy/dmypy.py | 2 ++ mypy/fastparse.py | 24 ++++++++++-------------- mypy/fastparse2.py | 24 ++++++++++-------------- mypy/main.py | 9 +-------- mypy/stubgen.py | 1 + mypy/util.py | 12 ++++++++++++ tox.ini | 3 +-- 7 files changed, 37 insertions(+), 38 deletions(-) diff --git a/mypy/dmypy.py b/mypy/dmypy.py index e4f77673f801..6896d84b5a91 100644 --- a/mypy/dmypy.py +++ b/mypy/dmypy.py @@ -18,6 +18,7 @@ from mypy.dmypy_util import DEFAULT_STATUS_FILE, receive from mypy.ipc import IPCClient, IPCException from mypy.dmypy_os import alive, kill +from mypy.util import check_python_version from mypy.version import __version__ @@ -131,6 +132,7 @@ class BadStatus(Exception): def main(argv: List[str]) -> None: """The code is top-down.""" + check_python_version('dmypy') args = parser.parse_args(argv) if not args.action: parser.print_usage() diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 577da287e421..9cad8eedf174 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -98,21 +98,17 @@ def ast3_parse(source: Union[str, bytes], filename: str, mode: str, NamedExpr = Any Constant = Any except ImportError: - if sys.version_info.minor > 4: - try: - from typed_ast import ast35 # type: ignore - except ImportError: - print('The typed_ast package is not installed.\n' - 'You can install it with `python3 -m pip install typed-ast`.', - file=sys.stderr) - else: - print('You need a more recent version of the typed_ast package.\n' - 'You can update to the latest version with ' - '`python3 -m pip install -U typed-ast`.', - file=sys.stderr) + try: + from typed_ast import ast35 # type: ignore + except ImportError: + print('The typed_ast package is not installed.\n' + 'You can install it with `python3 -m pip install typed-ast`.', + file=sys.stderr) else: - print('Mypy requires the typed_ast package, which is only compatible with\n' - 'Python 3.5 and greater.', file=sys.stderr) + print('You need a more recent version of the typed_ast package.\n' + 'You can update to the latest version with ' + '`python3 -m pip install -U typed-ast`.', + file=sys.stderr) sys.exit(1) N = TypeVar('N', bound=Node) diff --git a/mypy/fastparse2.py b/mypy/fastparse2.py index 40b7e6821994..365edeb00048 100644 --- a/mypy/fastparse2.py +++ b/mypy/fastparse2.py @@ -60,21 +60,17 @@ # Import ast3 from fastparse, which has special case for Python 3.8 from mypy.fastparse import ast3, ast3_parse except ImportError: - if sys.version_info.minor > 4: - try: - from typed_ast import ast35 # type: ignore - except ImportError: - print('The typed_ast package is not installed.\n' - 'You can install it with `python3 -m pip install typed-ast`.', - file=sys.stderr) - else: - print('You need a more recent version of the typed_ast package.\n' - 'You can update to the latest version with ' - '`python3 -m pip install -U typed-ast`.', - file=sys.stderr) + try: + from typed_ast import ast35 # type: ignore + except ImportError: + print('The typed_ast package is not installed.\n' + 'You can install it with `python3 -m pip install typed-ast`.', + file=sys.stderr) else: - print('Mypy requires the typed_ast package, which is only compatible with\n' - 'Python 3.5 and greater.', file=sys.stderr) + print('You need a more recent version of the typed_ast package.\n' + 'You can update to the latest version with ' + '`python3 -m pip install -U typed-ast`.', + file=sys.stderr) sys.exit(1) N = TypeVar('N', bound=Node) diff --git a/mypy/main.py b/mypy/main.py index 325a9c91608d..b7668b98d45f 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -53,14 +53,7 @@ def main(script_path: Optional[str], args: Optional[List[str]] = None) -> None: args: Custom command-line arguments. If not given, sys.argv[1:] will be used. """ - # Check for known bad Python versions. - if sys.version_info[:2] < (3, 5): - sys.exit("Running mypy with Python 3.4 or lower is not supported; " - "please upgrade to 3.5 or newer") - if sys.version_info[:3] == (3, 5, 0): - sys.exit("Running mypy with Python 3.5.0 is not supported; " - "please upgrade to 3.5.1 or newer") - + util.check_python_version('mypy') t0 = time.time() # To log stat() calls: os.stat = stat_proxy sys.setrecursionlimit(2 ** 14) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 6bf5c2e683f0..bfe4dea09964 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1181,6 +1181,7 @@ def parse_options(args: List[str]) -> Options: def main() -> None: + mypy.util.check_python_version('stubgen') # Make sure that the current directory is in sys.path so that # stubgen can be run on packages in the current directory. if not ('' in sys.path or '.' in sys.path): diff --git a/mypy/util.py b/mypy/util.py index 3455e9c8e70b..a2899261c5f1 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -257,3 +257,15 @@ def hard_exit(status: int = 0) -> None: def unmangle(name: str) -> str: """Remove internal suffixes from a short name.""" return name.rstrip("'") + + +def check_python_version(program: str) -> None: + """Report issues with the Python used to run mypy, dmypy, or stubgen""" + # Check for known bad Python versions. + if sys.version_info[:2] < (3, 5): + sys.exit("Running {name} with Python 3.4 or lower is not supported; " + "please upgrade to 3.5 or newer".format(name=program)) + # this can be deleted once we drop support for 3.5 + if sys.version_info[:3] == (3, 5, 0): + sys.exit("Running stubgen with Python 3.5.0 is not supported; " + "please upgrade to 3.5.1 or newer") diff --git a/tox.ini b/tox.ini index 3c5c18262b44..47197c1e87ae 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,7 @@ [tox] minversion = 3.5.1 skip_missing_interpreters = true -envlist = py34, - py35, +envlist = py35, py36, py37, lint, From ac28bdd122d14d4b08df10143b53fd42372e73f1 Mon Sep 17 00:00:00 2001 From: Ethan Smith Date: Tue, 26 Mar 2019 14:27:41 -0700 Subject: [PATCH 4/4] Fix omission of formatted string --- mypy/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/util.py b/mypy/util.py index a2899261c5f1..4daf7de5f0cc 100644 --- a/mypy/util.py +++ b/mypy/util.py @@ -267,5 +267,5 @@ def check_python_version(program: str) -> None: "please upgrade to 3.5 or newer".format(name=program)) # this can be deleted once we drop support for 3.5 if sys.version_info[:3] == (3, 5, 0): - sys.exit("Running stubgen with Python 3.5.0 is not supported; " - "please upgrade to 3.5.1 or newer") + sys.exit("Running {name} with Python 3.5.0 is not supported; " + "please upgrade to 3.5.1 or newer".format(name=program))