Skip to content

Commit 96525a2

Browse files
committed
Merge commit '9e45dadcf6d8dbab36f83d9df94a706c0b4f9207' into release-1.16
2 parents 8ed26e5 + 9e45dad commit 96525a2

File tree

199 files changed

+9103
-1834
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

199 files changed

+9103
-1834
lines changed

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ repos:
1111
- id: black
1212
exclude: '^(test-data/)'
1313
- repo: https://github.com/astral-sh/ruff-pre-commit
14-
rev: v0.8.6
14+
rev: v0.11.4
1515
hooks:
1616
- id: ruff
1717
args: [--exit-non-zero-on-fix]
1818
- repo: https://github.com/python-jsonschema/check-jsonschema
19-
rev: 0.31.0
19+
rev: 0.32.1
2020
hooks:
2121
- id: check-github-workflows
2222
- id: check-github-actions
@@ -43,7 +43,7 @@ repos:
4343
# but the integration only works if shellcheck is installed
4444
- "github.com/wasilibs/go-shellcheck/cmd/[email protected]"
4545
- repo: https://github.com/woodruffw/zizmor-pre-commit
46-
rev: v1.0.1
46+
rev: v1.5.2
4747
hooks:
4848
- id: zizmor
4949
- repo: local

CONTRIBUTING.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,14 @@ python runtests.py self
7676
# or equivalently:
7777
python -m mypy --config-file mypy_self_check.ini -p mypy
7878

79-
# Run a single test from the test suite
80-
pytest -n0 -k 'test_name'
79+
# Run a single test from the test suite (uses pytest substring expression matching)
80+
python runtests.py test_name
81+
# or equivalently:
82+
pytest -n0 -k test_name
8183

8284
# Run all test cases in the "test-data/unit/check-dataclasses.test" file
85+
python runtests.py check-dataclasses.test
86+
# or equivalently:
8387
pytest mypy/test/testcheck.py::TypeCheckSuite::check-dataclasses.test
8488

8589
# Run the formatters and linters

docs/source/command_line.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -749,8 +749,19 @@ of the above sections.
749749

750750
.. option:: --strict
751751

752-
This flag mode enables all optional error checking flags. You can see the
753-
list of flags enabled by strict mode in the full :option:`mypy --help` output.
752+
This flag mode enables a defined subset of optional error-checking flags.
753+
This subset primarily includes checks for inadvertent type unsoundness (i.e
754+
strict will catch type errors as long as intentional methods like type ignore
755+
or casting were not used.)
756+
757+
Note: the :option:`--warn-unreachable` flag
758+
is not automatically enabled by the strict flag.
759+
760+
The strict flag does not take precedence over other strict-related flags.
761+
Directly specifying a flag of alternate behavior will override the
762+
behavior of strict, regardless of the order in which they are passed.
763+
You can see the list of flags enabled by strict mode in the full
764+
:option:`mypy --help` output.
754765

755766
Note: the exact list of flags enabled by running :option:`--strict` may change
756767
over time.

docs/source/config_file.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ Platform configuration
432432

433433
Specifies the Python version used to parse and check the target
434434
program. The string should be in the format ``MAJOR.MINOR`` --
435-
for example ``2.7``. The default is the version of the Python
435+
for example ``3.9``. The default is the version of the Python
436436
interpreter used to run mypy.
437437

438438
This option may only be set in the global section (``[mypy]``).
@@ -1196,7 +1196,7 @@ of your repo (or append it to the end of an existing ``pyproject.toml`` file) an
11961196
# mypy global options:
11971197
11981198
[tool.mypy]
1199-
python_version = "2.7"
1199+
python_version = "3.9"
12001200
warn_return_any = true
12011201
warn_unused_configs = true
12021202
exclude = [

docs/source/runtime_troubles.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ version of Python considers legal code. This section describes these scenarios
88
and explains how to get your code running again. Generally speaking, we have
99
three tools at our disposal:
1010

11-
* Use of ``from __future__ import annotations`` (:pep:`563`)
12-
(this behaviour may eventually be made the default in a future Python version)
1311
* Use of string literal types or type comments
1412
* Use of ``typing.TYPE_CHECKING``
13+
* Use of ``from __future__ import annotations`` (:pep:`563`)
1514

1615
We provide a description of these before moving onto discussion of specific
1716
problems you may encounter.

docs/source/type_narrowing.rst

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ techniques which are supported by mypy.
88

99
Type narrowing is when you convince a type checker that a broader type is actually more specific, for instance, that an object of type ``Shape`` is actually of the narrower type ``Square``.
1010

11+
The following type narrowing techniques are available:
12+
13+
- :ref:`type-narrowing-expressions`
14+
- :ref:`casts`
15+
- :ref:`type-guards`
16+
- :ref:`typeis`
17+
18+
19+
.. _type-narrowing-expressions:
1120

1221
Type narrowing expressions
1322
--------------------------
@@ -356,40 +365,6 @@ What happens here?
356365

357366
The same will work with ``isinstance(x := a, float)`` as well.
358367

359-
Limitations
360-
-----------
361-
362-
Mypy's analysis is limited to individual symbols and it will not track
363-
relationships between symbols. For example, in the following code
364-
it's easy to deduce that if :code:`a` is None then :code:`b` must not be,
365-
therefore :code:`a or b` will always be an instance of :code:`C`,
366-
but Mypy will not be able to tell that:
367-
368-
.. code-block:: python
369-
370-
class C:
371-
pass
372-
373-
def f(a: C | None, b: C | None) -> C:
374-
if a is not None or b is not None:
375-
return a or b # Incompatible return value type (got "C | None", expected "C")
376-
return C()
377-
378-
Tracking these sort of cross-variable conditions in a type checker would add significant complexity
379-
and performance overhead.
380-
381-
You can use an ``assert`` to convince the type checker, override it with a :ref:`cast <casts>`
382-
or rewrite the function to be slightly more verbose:
383-
384-
.. code-block:: python
385-
386-
def f(a: C | None, b: C | None) -> C:
387-
if a is not None:
388-
return a
389-
elif b is not None:
390-
return b
391-
return C()
392-
393368

394369
.. _typeis:
395370

@@ -555,3 +530,38 @@ You can use the assignment expression operator ``:=`` with ``TypeIs`` to create
555530
reveal_type(x) # Revealed type is 'float'
556531
# x is narrowed to float in this block
557532
print(x + 1.0)
533+
534+
535+
Limitations
536+
-----------
537+
538+
Mypy's analysis is limited to individual symbols and it will not track
539+
relationships between symbols. For example, in the following code
540+
it's easy to deduce that if :code:`a` is None then :code:`b` must not be,
541+
therefore :code:`a or b` will always be an instance of :code:`C`,
542+
but Mypy will not be able to tell that:
543+
544+
.. code-block:: python
545+
546+
class C:
547+
pass
548+
549+
def f(a: C | None, b: C | None) -> C:
550+
if a is not None or b is not None:
551+
return a or b # Incompatible return value type (got "C | None", expected "C")
552+
return C()
553+
554+
Tracking these sort of cross-variable conditions in a type checker would add significant complexity
555+
and performance overhead.
556+
557+
You can use an ``assert`` to convince the type checker, override it with a :ref:`cast <casts>`
558+
or rewrite the function to be slightly more verbose:
559+
560+
.. code-block:: python
561+
562+
def f(a: C | None, b: C | None) -> C:
563+
if a is not None:
564+
return a
565+
elif b is not None:
566+
return b
567+
return C()

misc/perf_compare.py

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* Create a temp clone of the mypy repo for each target commit to measure
1010
* Checkout a target commit in each of the clones
1111
* Compile mypyc in each of the clones *in parallel*
12-
* Create another temp clone of the mypy repo as the code to check
12+
* Create another temp clone of the first provided revision (or, with -r, a foreign repo) as the code to check
1313
* Self check with each of the compiled mypys N times
1414
* Report the average runtimes and relative performance
1515
* Remove the temp clones
@@ -44,13 +44,15 @@ def build_mypy(target_dir: str) -> None:
4444
subprocess.run(cmd, env=env, check=True, cwd=target_dir)
4545

4646

47-
def clone(target_dir: str, commit: str | None) -> None:
48-
heading(f"Cloning mypy to {target_dir}")
49-
repo_dir = os.getcwd()
47+
def clone(target_dir: str, commit: str | None, repo_source: str | None = None) -> None:
48+
source_name = repo_source or "mypy"
49+
heading(f"Cloning {source_name} to {target_dir}")
50+
if repo_source is None:
51+
repo_source = os.getcwd()
5052
if os.path.isdir(target_dir):
5153
print(f"{target_dir} exists: deleting")
5254
shutil.rmtree(target_dir)
53-
subprocess.run(["git", "clone", repo_dir, target_dir], check=True)
55+
subprocess.run(["git", "clone", repo_source, target_dir], check=True)
5456
if commit:
5557
subprocess.run(["git", "checkout", commit], check=True, cwd=target_dir)
5658

@@ -64,7 +66,7 @@ def edit_python_file(fnam: str) -> None:
6466

6567

6668
def run_benchmark(
67-
compiled_dir: str, check_dir: str, *, incremental: bool, code: str | None
69+
compiled_dir: str, check_dir: str, *, incremental: bool, code: str | None, foreign: bool | None
6870
) -> float:
6971
cache_dir = os.path.join(compiled_dir, ".mypy_cache")
7072
if os.path.isdir(cache_dir) and not incremental:
@@ -76,6 +78,8 @@ def run_benchmark(
7678
cmd = [sys.executable, "-m", "mypy"]
7779
if code:
7880
cmd += ["-c", code]
81+
elif foreign:
82+
pass
7983
else:
8084
cmd += ["--config-file", os.path.join(abschk, "mypy_self_check.ini")]
8185
cmd += glob.glob(os.path.join(abschk, "mypy/*.py"))
@@ -86,18 +90,33 @@ def run_benchmark(
8690
edit_python_file(os.path.join(abschk, "mypy/test/testcheck.py"))
8791
t0 = time.time()
8892
# Ignore errors, since some commits being measured may generate additional errors.
89-
subprocess.run(cmd, cwd=compiled_dir, env=env)
93+
if foreign:
94+
subprocess.run(cmd, cwd=check_dir, env=env)
95+
else:
96+
subprocess.run(cmd, cwd=compiled_dir, env=env)
9097
return time.time() - t0
9198

9299

93100
def main() -> None:
94-
parser = argparse.ArgumentParser()
101+
whole_program_time_0 = time.time()
102+
parser = argparse.ArgumentParser(
103+
formatter_class=argparse.RawDescriptionHelpFormatter,
104+
description=__doc__,
105+
epilog="Remember: you usually want the first argument to this command to be 'master'.",
106+
)
95107
parser.add_argument(
96108
"--incremental",
97109
default=False,
98110
action="store_true",
99111
help="measure incremental run (fully cached)",
100112
)
113+
parser.add_argument(
114+
"--dont-setup",
115+
default=False,
116+
action="store_true",
117+
help="don't make the clones or compile mypy, just run the performance measurement benchmark "
118+
+ "(this will fail unless the clones already exist, such as from a previous run that was canceled before it deleted them)",
119+
)
101120
parser.add_argument(
102121
"--num-runs",
103122
metavar="N",
@@ -112,42 +131,65 @@ def main() -> None:
112131
type=int,
113132
help="set maximum number of parallel builds (default=8)",
114133
)
134+
parser.add_argument(
135+
"-r",
136+
metavar="FOREIGN_REPOSITORY",
137+
default=None,
138+
type=str,
139+
help="measure time to typecheck the project at FOREIGN_REPOSITORY instead of mypy self-check; "
140+
+ "the provided value must be the URL or path of a git repo "
141+
+ "(note that this script will take no special steps to *install* the foreign repo, so you will probably get a lot of missing import errors)",
142+
)
115143
parser.add_argument(
116144
"-c",
117145
metavar="CODE",
118146
default=None,
119147
type=str,
120148
help="measure time to type check Python code fragment instead of mypy self-check",
121149
)
122-
parser.add_argument("commit", nargs="+", help="git revision to measure (e.g. branch name)")
150+
parser.add_argument(
151+
"commit",
152+
nargs="+",
153+
help="git revision(s), e.g. branch name or commit id, to measure the performance of",
154+
)
123155
args = parser.parse_args()
124156
incremental: bool = args.incremental
157+
dont_setup: bool = args.dont_setup
125158
commits = args.commit
126159
num_runs: int = args.num_runs + 1
127160
max_workers: int = args.j
128161
code: str | None = args.c
162+
foreign_repo: str | None = args.r
129163

130164
if not (os.path.isdir(".git") and os.path.isdir("mypyc")):
131-
sys.exit("error: Run this the mypy repo root")
165+
sys.exit("error: You must run this script from the mypy repo root")
132166

133167
target_dirs = []
134168
for i, commit in enumerate(commits):
135169
target_dir = f"mypy.{i}.tmpdir"
136170
target_dirs.append(target_dir)
137-
clone(target_dir, commit)
171+
if not dont_setup:
172+
clone(target_dir, commit)
138173

139-
self_check_dir = "mypy.self.tmpdir"
140-
clone(self_check_dir, commits[0])
174+
if foreign_repo:
175+
check_dir = "mypy.foreign.tmpdir"
176+
if not dont_setup:
177+
clone(check_dir, None, foreign_repo)
178+
else:
179+
check_dir = "mypy.self.tmpdir"
180+
if not dont_setup:
181+
clone(check_dir, commits[0])
141182

142-
heading("Compiling mypy")
143-
print("(This will take a while...)")
183+
if not dont_setup:
184+
heading("Compiling mypy")
185+
print("(This will take a while...)")
144186

145-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
146-
futures = [executor.submit(build_mypy, target_dir) for target_dir in target_dirs]
147-
for future in as_completed(futures):
148-
future.result()
187+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
188+
futures = [executor.submit(build_mypy, target_dir) for target_dir in target_dirs]
189+
for future in as_completed(futures):
190+
future.result()
149191

150-
print(f"Finished compiling mypy ({len(commits)} builds)")
192+
print(f"Finished compiling mypy ({len(commits)} builds)")
151193

152194
heading("Performing measurements")
153195

@@ -160,7 +202,13 @@ def main() -> None:
160202
items = list(enumerate(commits))
161203
random.shuffle(items)
162204
for i, commit in items:
163-
tt = run_benchmark(target_dirs[i], self_check_dir, incremental=incremental, code=code)
205+
tt = run_benchmark(
206+
target_dirs[i],
207+
check_dir,
208+
incremental=incremental,
209+
code=code,
210+
foreign=bool(foreign_repo),
211+
)
164212
# Don't record the first warm-up run
165213
if n > 0:
166214
print(f"{commit}: t={tt:.3f}s")
@@ -171,15 +219,28 @@ def main() -> None:
171219
first = -1.0
172220
for commit in commits:
173221
tt = statistics.mean(results[commit])
222+
# pstdev (instead of stdev) is used here primarily to accommodate the case where num_runs=1
223+
s = statistics.pstdev(results[commit]) if len(results[commit]) > 1 else 0
174224
if first < 0:
175225
delta = "0.0%"
176226
first = tt
177227
else:
178228
d = (tt / first) - 1
179229
delta = f"{d:+.1%}"
180-
print(f"{commit:<25} {tt:.3f}s ({delta})")
230+
print(f"{commit:<25} {tt:.3f}s ({delta}) | stdev {s:.3f}s ")
231+
232+
t = int(time.time() - whole_program_time_0)
233+
total_time_taken_formatted = ", ".join(
234+
f"{v} {n if v==1 else n+'s'}"
235+
for v, n in ((t // 3600, "hour"), (t // 60 % 60, "minute"), (t % 60, "second"))
236+
if v
237+
)
238+
print(
239+
"Total time taken by the whole benchmarking program (including any setup):",
240+
total_time_taken_formatted,
241+
)
181242

182-
shutil.rmtree(self_check_dir)
243+
shutil.rmtree(check_dir)
183244
for target_dir in target_dirs:
184245
shutil.rmtree(target_dir)
185246

0 commit comments

Comments
 (0)