Skip to content

Commit 9b85a8c

Browse files
authored
Merge branch 'master' into neopexrl-patch-1
2 parents d032818 + 566ba1e commit 9b85a8c

101 files changed

Lines changed: 4157 additions & 667 deletions

File tree

Some content is hidden

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

.github/workflows/test.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,18 @@ jobs:
7979
# # allow_failure: true
8080
# test_mypyc: true
8181

82-
- name: mypyc runtime tests with py39-macos
83-
python: '3.9.21'
84-
# TODO: macos-13 is the last one to support Python 3.9, change it to macos-latest when updating the Python version
85-
os: macos-13
82+
- name: mypyc runtime tests with py313-macos
83+
python: '3.13'
84+
os: macos-latest
8685
toxenv: py
8786
tox_extra_args: "-n 3 mypyc/test/test_run.py mypyc/test/test_external.py"
87+
88+
- name: mypyc runtime tests with py310-ubuntu
89+
python: '3.10'
90+
os: ubuntu-latest
91+
toxenv: py
92+
tox_extra_args: "-n 3 mypyc/test/test_run.py mypyc/test/test_external.py"
93+
8894
# This is broken. See
8995
# - https://github.com/python/mypy/issues/17819
9096
# - https://github.com/python/mypy/pull/17822

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ repos:
66
- id: trailing-whitespace
77
- id: end-of-file-fixer
88
- repo: https://github.com/psf/black-pre-commit-mirror
9-
rev: 25.1.0
9+
rev: 25.9.0
1010
hooks:
1111
- id: black
1212
exclude: '^(test-data/)'
1313
- repo: https://github.com/astral-sh/ruff-pre-commit
14-
rev: v0.11.4
14+
rev: v0.14.3
1515
hooks:
16-
- id: ruff
16+
- id: ruff-check
1717
args: [--exit-non-zero-on-fix]
1818
- repo: https://github.com/python-jsonschema/check-jsonschema
1919
rev: 0.32.1

docs/source/command_line.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,7 +1159,7 @@ format into the specified directory.
11591159
Enabling incomplete/experimental features
11601160
*****************************************
11611161

1162-
.. option:: --enable-incomplete-feature {PreciseTupleTypes, InlineTypedDict}
1162+
.. option:: --enable-incomplete-feature {PreciseTupleTypes,InlineTypedDict,TypeForm}
11631163

11641164
Some features may require several mypy releases to implement, for example
11651165
due to their complexity, potential for backwards incompatibility, or
@@ -1211,8 +1211,11 @@ List of currently incomplete/experimental features:
12111211

12121212
.. code-block:: python
12131213
1214-
def test_values() -> {"int": int, "str": str}:
1215-
return {"int": 42, "str": "test"}
1214+
def test_values() -> {"width": int, "description": str}:
1215+
return {"width": 42, "description": "test"}
1216+
1217+
* ``TypeForm``: this feature enables ``TypeForm``, as described in
1218+
`PEP 747 – Annotating Type Forms <https://peps.python.org/pep-0747/>_`.
12161219

12171220

12181221
Miscellaneous

docs/source/duck_type_compatibility.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ supported for a small set of built-in types:
99
* ``int`` is duck type compatible with ``float`` and ``complex``.
1010
* ``float`` is duck type compatible with ``complex``.
1111
* ``bytearray`` and ``memoryview`` are duck type compatible with ``bytes``.
12+
(this will be disabled by default in **mypy 2.0**, and currently can be
13+
disabled with :option:`--strict-bytes <mypy --strict-bytes>`.)
1214

1315
For example, mypy considers an ``int`` object to be valid whenever a
1416
``float`` object is expected. Thus code like this is nice and clean

docs/source/error_code_list.rst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,65 @@ type must be a subtype of the original type::
12861286
def g(x: object) -> TypeIs[str]: # OK
12871287
...
12881288

1289+
.. _code-maybe-unrecognized-str-typeform:
1290+
1291+
String appears in a context which expects a TypeForm [maybe-unrecognized-str-typeform]
1292+
--------------------------------------------------------------------------------------
1293+
1294+
TypeForm literals may contain string annotations:
1295+
1296+
.. code-block:: python
1297+
1298+
typx1: TypeForm = str | None
1299+
typx2: TypeForm = 'str | None' # OK
1300+
typx3: TypeForm = 'str' | None # OK
1301+
1302+
However TypeForm literals containing a string annotation can only be recognized
1303+
by mypy in the following locations:
1304+
1305+
.. code-block:: python
1306+
1307+
typx_var: TypeForm = 'str | None' # assignment r-value
1308+
1309+
def func(typx_param: TypeForm) -> TypeForm:
1310+
return 'str | None' # returned expression
1311+
1312+
func('str | None') # callable's argument
1313+
1314+
If you try to use a string annotation in some other location
1315+
which expects a TypeForm, the string value will always be treated as a ``str``
1316+
even if a ``TypeForm`` would be more appropriate and this error code
1317+
will be generated:
1318+
1319+
.. code-block:: python
1320+
1321+
# Error: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. [maybe-unrecognized-str-typeform]
1322+
# Error: List item 0 has incompatible type "str"; expected "TypeForm[Any]" [list-item]
1323+
list_of_typx: list[TypeForm] = ['str | None', float]
1324+
1325+
Fix the error by surrounding the entire type with ``TypeForm(...)``:
1326+
1327+
.. code-block:: python
1328+
1329+
list_of_typx: list[TypeForm] = [TypeForm('str | None'), float] # OK
1330+
1331+
Similarly, if you try to use a string literal in a location which expects a
1332+
TypeForm, this error code will be generated:
1333+
1334+
.. code-block:: python
1335+
1336+
dict_of_typx = {'str_or_none': TypeForm(str | None)}
1337+
# Error: TypeForm containing a string annotation cannot be recognized here. Surround with TypeForm(...) to recognize. [maybe-unrecognized-str-typeform]
1338+
list_of_typx: list[TypeForm] = [dict_of_typx['str_or_none']]
1339+
1340+
Fix the error by adding ``# type: ignore[maybe-unrecognized-str-typeform]``
1341+
to the line with the string literal:
1342+
1343+
.. code-block:: python
1344+
1345+
dict_of_typx = {'str_or_none': TypeForm(str | None)}
1346+
list_of_typx: list[TypeForm] = [dict_of_typx['str_or_none']] # type: ignore[maybe-unrecognized-str-typeform]
1347+
12891348
.. _code-misc:
12901349

12911350
Miscellaneous checks [misc]

docs/source/kinds_of_types.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ isn't supported by the runtime with some limitations, if you use
294294
def f(x: int | str) -> None: # OK on Python 3.7 and later
295295
...
296296
297+
.. _no-strict-optional:
297298
.. _strict_optional:
298299

299300
Optional types and the None type

docs/source/typed_dict.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,8 @@ to use inline TypedDict syntax. For example:
303303

304304
.. code-block:: python
305305
306-
def test_values() -> {"int": int, "str": str}:
307-
return {"int": 42, "str": "test"}
306+
def test_values() -> {"width": int, "description": str}:
307+
return {"width": 42, "description": "test"}
308308
309309
class Response(TypedDict):
310310
status: int

misc/analyze_typeform_stats.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Analyze TypeForm parsing efficiency from mypy build stats.
4+
5+
Usage:
6+
python3 analyze_typeform_stats.py '<mypy_output_with_stats>'
7+
python3 -m mypy --dump-build-stats file.py 2>&1 | python3 analyze_typeform_stats.py
8+
9+
Example output:
10+
TypeForm Expression Parsing Statistics:
11+
==================================================
12+
Total calls to SA.try_parse_as_type_expression: 14,555
13+
Quick rejections (no full parse): 14,255
14+
Full parses attempted: 300
15+
- Successful: 248
16+
- Failed: 52
17+
18+
Efficiency Metrics:
19+
- Quick rejection rate: 97.9%
20+
- Full parse rate: 2.1%
21+
- Full parse success rate: 82.7%
22+
- Overall success rate: 1.7%
23+
24+
Performance Implications:
25+
- Expensive failed full parses: 52 (0.4% of all calls)
26+
27+
See also:
28+
- mypy/semanal.py: SemanticAnalyzer.try_parse_as_type_expression()
29+
- mypy/semanal.py: DEBUG_TYPE_EXPRESSION_FULL_PARSE_FAILURES
30+
"""
31+
32+
import re
33+
import sys
34+
35+
36+
def analyze_stats(output: str) -> None:
37+
"""Parse mypy stats output and calculate TypeForm parsing efficiency."""
38+
39+
# Extract the three counters
40+
total_match = re.search(r"type_expression_parse_count:\s*(\d+)", output)
41+
success_match = re.search(r"type_expression_full_parse_success_count:\s*(\d+)", output)
42+
failure_match = re.search(r"type_expression_full_parse_failure_count:\s*(\d+)", output)
43+
44+
if not (total_match and success_match and failure_match):
45+
print("Error: Could not find all required counters in output")
46+
return
47+
48+
total = int(total_match.group(1))
49+
successes = int(success_match.group(1))
50+
failures = int(failure_match.group(1))
51+
52+
full_parses = successes + failures
53+
54+
print("TypeForm Expression Parsing Statistics:")
55+
print("=" * 50)
56+
print(f"Total calls to SA.try_parse_as_type_expression: {total:,}")
57+
print(f"Quick rejections (no full parse): {total - full_parses:,}")
58+
print(f"Full parses attempted: {full_parses:,}")
59+
print(f" - Successful: {successes:,}")
60+
print(f" - Failed: {failures:,}")
61+
if total > 0:
62+
print()
63+
print("Efficiency Metrics:")
64+
print(f" - Quick rejection rate: {((total - full_parses) / total * 100):.1f}%")
65+
print(f" - Full parse rate: {(full_parses / total * 100):.1f}%")
66+
print(f" - Full parse success rate: {(successes / full_parses * 100):.1f}%")
67+
print(f" - Overall success rate: {(successes / total * 100):.1f}%")
68+
print()
69+
print("Performance Implications:")
70+
print(
71+
f" - Expensive failed full parses: {failures:,} ({(failures / total * 100):.1f}% of all calls)"
72+
)
73+
74+
75+
if __name__ == "__main__":
76+
if len(sys.argv) == 1:
77+
# Read from stdin
78+
output = sys.stdin.read()
79+
elif len(sys.argv) == 2:
80+
# Read from command line argument
81+
output = sys.argv[1]
82+
else:
83+
print("Usage: python3 analyze_typeform_stats.py [mypy_output_with_stats]")
84+
print("Examples:")
85+
print(
86+
" python3 -m mypy --dump-build-stats file.py 2>&1 | python3 analyze_typeform_stats.py"
87+
)
88+
print(" python3 analyze_typeform_stats.py 'output_string'")
89+
sys.exit(1)
90+
91+
analyze_stats(output)

mypy-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ typing_extensions>=4.6.0
44
mypy_extensions>=1.0.0
55
pathspec>=0.9.0
66
tomli>=1.1.0; python_version<'3.11'
7-
librt>=0.3.0
7+
librt>=0.4.0

mypy/build.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Final, NoReturn, TextIO, TypedDict
2929
from typing_extensions import TypeAlias as _TypeAlias
3030

31+
from librt.internal import cache_version
32+
3133
import mypy.semanal_main
32-
from mypy.cache import Buffer, CacheMeta
34+
from mypy.cache import CACHE_VERSION, Buffer, CacheMeta
3335
from mypy.checker import TypeChecker
3436
from mypy.error_formatter import OUTPUT_CHOICES, ErrorFormatter
3537
from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error
@@ -601,6 +603,7 @@ def __init__(
601603
self.options = options
602604
self.version_id = version_id
603605
self.modules: dict[str, MypyFile] = {}
606+
self.import_map: dict[str, set[str]] = {}
604607
self.missing_modules: set[str] = set()
605608
self.fg_deps_meta: dict[str, FgDepMeta] = {}
606609
# fg_deps holds the dependencies of every module that has been
@@ -621,6 +624,7 @@ def __init__(
621624
self.incomplete_namespaces,
622625
self.errors,
623626
self.plugin,
627+
self.import_map,
624628
)
625629
self.all_types: dict[Expression, Type] = {} # Enabled by export_types
626630
self.indirection_detector = TypeIndirectionVisitor()
@@ -740,6 +744,26 @@ def getmtime(self, path: str) -> int:
740744
else:
741745
return int(self.metastore.getmtime(path))
742746

747+
def correct_rel_imp(self, file: MypyFile, imp: ImportFrom | ImportAll) -> str:
748+
"""Function to correct for relative imports."""
749+
file_id = file.fullname
750+
rel = imp.relative
751+
if rel == 0:
752+
return imp.id
753+
if os.path.basename(file.path).startswith("__init__."):
754+
rel -= 1
755+
if rel != 0:
756+
file_id = ".".join(file_id.split(".")[:-rel])
757+
new_id = file_id + "." + imp.id if imp.id else file_id
758+
759+
if not new_id:
760+
self.errors.set_file(file.path, file.name, self.options)
761+
self.errors.report(
762+
imp.line, 0, "No parent module -- cannot perform relative import", blocker=True
763+
)
764+
765+
return new_id
766+
743767
def all_imported_modules_in_file(self, file: MypyFile) -> list[tuple[int, str, int]]:
744768
"""Find all reachable import statements in a file.
745769
@@ -748,27 +772,6 @@ def all_imported_modules_in_file(self, file: MypyFile) -> list[tuple[int, str, i
748772
749773
Can generate blocking errors on bogus relative imports.
750774
"""
751-
752-
def correct_rel_imp(imp: ImportFrom | ImportAll) -> str:
753-
"""Function to correct for relative imports."""
754-
file_id = file.fullname
755-
rel = imp.relative
756-
if rel == 0:
757-
return imp.id
758-
if os.path.basename(file.path).startswith("__init__."):
759-
rel -= 1
760-
if rel != 0:
761-
file_id = ".".join(file_id.split(".")[:-rel])
762-
new_id = file_id + "." + imp.id if imp.id else file_id
763-
764-
if not new_id:
765-
self.errors.set_file(file.path, file.name, self.options)
766-
self.errors.report(
767-
imp.line, 0, "No parent module -- cannot perform relative import", blocker=True
768-
)
769-
770-
return new_id
771-
772775
res: list[tuple[int, str, int]] = []
773776
for imp in file.imports:
774777
if not imp.is_unreachable:
@@ -783,7 +786,7 @@ def correct_rel_imp(imp: ImportFrom | ImportAll) -> str:
783786
ancestors.append(part)
784787
res.append((ancestor_pri, ".".join(ancestors), imp.line))
785788
elif isinstance(imp, ImportFrom):
786-
cur_id = correct_rel_imp(imp)
789+
cur_id = self.correct_rel_imp(file, imp)
787790
all_are_submodules = True
788791
# Also add any imported names that are submodules.
789792
pri = import_priority(imp, PRI_MED)
@@ -803,7 +806,7 @@ def correct_rel_imp(imp: ImportFrom | ImportAll) -> str:
803806
res.append((pri, cur_id, imp.line))
804807
elif isinstance(imp, ImportAll):
805808
pri = import_priority(imp, PRI_HIGH)
806-
res.append((pri, correct_rel_imp(imp), imp.line))
809+
res.append((pri, self.correct_rel_imp(file, imp), imp.line))
807810

808811
# Sort such that module (e.g. foo.bar.baz) comes before its ancestors (e.g. foo
809812
# and foo.bar) so that, if FindModuleCache finds the target module in a
@@ -1334,12 +1337,18 @@ def find_cache_meta(id: str, path: str, manager: BuildManager) -> CacheMeta | No
13341337
return None
13351338
t1 = time.time()
13361339
if isinstance(meta, bytes):
1337-
data_io = Buffer(meta)
1340+
# If either low-level buffer format or high-level cache layout changed, we
1341+
# cannot use the cache files, even with --skip-version-check.
1342+
# TODO: switch to something like librt.internal.read_byte() if this is slow.
1343+
if meta[0] != cache_version() or meta[1] != CACHE_VERSION:
1344+
manager.log(f"Metadata abandoned for {id}: incompatible cache format")
1345+
return None
1346+
data_io = Buffer(meta[2:])
13381347
m = CacheMeta.read(data_io, data_file)
13391348
else:
13401349
m = CacheMeta.deserialize(meta, data_file)
13411350
if m is None:
1342-
manager.log(f"Metadata abandoned for {id}: attributes are missing")
1351+
manager.log(f"Metadata abandoned for {id}: cannot deserialize data")
13431352
return None
13441353
t2 = time.time()
13451354
manager.add_stats(
@@ -1671,7 +1680,9 @@ def write_cache_meta(meta: CacheMeta, manager: BuildManager, meta_file: str) ->
16711680
if manager.options.fixed_format_cache:
16721681
data_io = Buffer()
16731682
meta.write(data_io)
1674-
meta_bytes = data_io.getvalue()
1683+
# Prefix with both low- and high-level cache format versions for future validation.
1684+
# TODO: switch to something like librt.internal.write_byte() if this is slow.
1685+
meta_bytes = bytes([cache_version(), CACHE_VERSION]) + data_io.getvalue()
16751686
else:
16761687
meta_dict = meta.serialize()
16771688
meta_bytes = json_dumps(meta_dict, manager.options.debug_cache)
@@ -2888,6 +2899,9 @@ def dispatch(sources: list[BuildSource], manager: BuildManager, stdout: TextIO)
28882899
manager.cache_enabled = False
28892900
graph = load_graph(sources, manager)
28902901

2902+
for id in graph:
2903+
manager.import_map[id] = set(graph[id].dependencies + graph[id].suppressed)
2904+
28912905
t1 = time.time()
28922906
manager.add_stats(
28932907
graph_size=len(graph),

0 commit comments

Comments
 (0)