Skip to content

Commit ef11a45

Browse files
committed
Merge remote-tracking branch 'upstream/master' into bugfix/gh-19036-walrus-conditions
2 parents 43f8498 + 8c772c7 commit ef11a45

File tree

303 files changed

+5615
-4914
lines changed

Some content is hidden

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

303 files changed

+5615
-4914
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ jobs:
167167
echo debug build; python -c 'import sysconfig; print(bool(sysconfig.get_config_var("Py_DEBUG")))'
168168
echo os.cpu_count; python -c 'import os; print(os.cpu_count())'
169169
echo os.sched_getaffinity; python -c 'import os; print(len(getattr(os, "sched_getaffinity", lambda *args: [])(0)))'
170-
pip install tox==4.21.2
170+
pip install setuptools==75.1.0 tox==4.21.2
171171
172172
- name: Compiled with mypyc
173173
if: ${{ matrix.test_mypyc }}
@@ -230,7 +230,7 @@ jobs:
230230
default: 3.11.1
231231
command: python -c "import platform; print(f'{platform.architecture()=} {platform.machine()=}');"
232232
- name: Install tox
233-
run: pip install tox==4.21.2
233+
run: pip install setuptools==75.1.0 tox==4.21.2
234234
- name: Setup tox environment
235235
run: tox run -e py --notest
236236
- name: Test

CHANGELOG.md

Lines changed: 405 additions & 10 deletions
Large diffs are not rendered by default.

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: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -593,12 +593,58 @@ of the above sections.
593593
This flag causes mypy to suppress errors caused by not being able to fully
594594
infer the types of global and class variables.
595595

596-
.. option:: --allow-redefinition
596+
.. option:: --allow-redefinition-new
597597

598598
By default, mypy won't allow a variable to be redefined with an
599-
unrelated type. This flag enables redefinition of a variable with an
599+
unrelated type. This *experimental* flag enables the redefinition of
600+
unannotated variables with an arbitrary type. You will also need to enable
601+
:option:`--local-partial-types <mypy --local-partial-types>`.
602+
Example:
603+
604+
.. code-block:: python
605+
606+
def maybe_convert(n: int, b: bool) -> int | str:
607+
if b:
608+
x = str(n) # Assign "str"
609+
else:
610+
x = n # Assign "int"
611+
# Type of "x" is "int | str" here.
612+
return x
613+
614+
Without the new flag, mypy only supports inferring optional types
615+
(``X | None``) from multiple assignments. With this option enabled,
616+
mypy can infer arbitrary union types.
617+
618+
This also enables an unannotated variable to have different types in different
619+
code locations:
620+
621+
.. code-block:: python
622+
623+
if check():
624+
for x in range(n):
625+
# Type of "x" is "int" here.
626+
...
627+
else:
628+
for x in ['a', 'b']:
629+
# Type of "x" is "str" here.
630+
...
631+
632+
Note: We are planning to turn this flag on by default in a future mypy
633+
release, along with :option:`--local-partial-types <mypy --local-partial-types>`.
634+
The feature is still experimental, and the semantics may still change.
635+
636+
.. option:: --allow-redefinition
637+
638+
This is an older variant of
639+
:option:`--allow-redefinition-new <mypy --allow-redefinition-new>`.
640+
This flag enables redefinition of a variable with an
600641
arbitrary type *in some contexts*: only redefinitions within the
601642
same block and nesting depth as the original definition are allowed.
643+
644+
We have no plans to remove this flag, but we expect that
645+
:option:`--allow-redefinition-new <mypy --allow-redefinition-new>`
646+
will replace this flag for new use cases eventually.
647+
602648
Example where this can be useful:
603649

604650
.. code-block:: python

docs/source/config_file.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,44 @@ section of the command line docs.
713713
Causes mypy to suppress errors caused by not being able to fully
714714
infer the types of global and class variables.
715715

716+
.. confval:: allow_redefinition_new
717+
718+
:type: boolean
719+
:default: False
720+
721+
By default, mypy won't allow a variable to be redefined with an
722+
unrelated type. This *experimental* flag enables the redefinition of
723+
unannotated variables with an arbitrary type. You will also need to enable
724+
:confval:`local_partial_types`.
725+
Example:
726+
727+
.. code-block:: python
728+
729+
def maybe_convert(n: int, b: bool) -> int | str:
730+
if b:
731+
x = str(n) # Assign "str"
732+
else:
733+
x = n # Assign "int"
734+
# Type of "x" is "int | str" here.
735+
return x
736+
737+
This also enables an unannotated variable to have different types in different
738+
code locations:
739+
740+
.. code-block:: python
741+
742+
if check():
743+
for x in range(n):
744+
# Type of "x" is "int" here.
745+
...
746+
else:
747+
for x in ['a', 'b']:
748+
# Type of "x" is "str" here.
749+
...
750+
751+
Note: We are planning to turn this flag on by default in a future mypy
752+
release, along with :confval:`local_partial_types`.
753+
716754
.. confval:: allow_redefinition
717755

718756
:type: boolean
@@ -746,6 +784,7 @@ section of the command line docs.
746784

747785
Disallows inferring variable type for ``None`` from two assignments in different scopes.
748786
This is always implicitly enabled when using the :ref:`mypy daemon <mypy_daemon>`.
787+
This will be enabled by default in a future mypy release.
749788

750789
.. confval:: disable_error_code
751790

docs/source/generics.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Using ``Stack`` is similar to built-in container types:
9393
stack.push('x')
9494
9595
stack2: Stack[str] = Stack()
96-
stack2.append('x')
96+
stack2.push('x')
9797
9898
Construction of instances of generic types is type checked (Python 3.12 syntax):
9999

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)