An experiment in disallowing expression statements (or "unused values"), must_use/MustUse etc.
Chưa có ai nhận issue này.
- Ngôn ngữ chính
- Python
- Star
- 20.6k
- Fork
- 3.3k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
Having worked with Rust for a while I grew fond of Rust telling me I'm not using some value returned from a function. After reading https://github.com/python/mypy/issues/6936 I started thinking about a possible approach to get something similar in Mypy, possibly by adding metadata to return types using PEP 593's Annotated:
_must_use_flag = object()
_T = TypeVar('_T')
MustUse = Annotated[_T, _must_use_flag]
# May perform a partial write
def write_bytes(data: bytes) -> MustUse[int]:
# ...
But then I considered it some more and figured out that really the majority of return values from the functions I deal with should be handled one way or another, so it's possible the "must use" behavior should be the default. Well, not necessarily default-default, but gated with a Mypy flag possibly. I implemented a dirty version of this where an error is raised on every expression in statement context (not only function calls), you can find it in the following commit: https://github.com/jstasiak/mypy/commit/8e8667b8b7910a5b9284bd0cc9850acb4e181c21 (I'm skipping literal values reporting /because docstrings/, ellipsis (because it can't possibly be an error in this context) and -values).
I also added some logging code in the same branch so I can gather some stats and open discussion on this: https://github.com/jstasiak/mypy/commit/d685a010c26d7740ae0839b8ba84196e034c5c68
I ran mypy self-test with this and here are the results:
# The number of all expressions encountered
% wc -l expr_logger.txt
226248 expr_logger.txt
# The number of disallowed expression statements
% wc -l stmt_logger.txt
409 stmt_logger.txt
So ~0.18% of all expressions in Mypy are currently used in statement context. The counts of specific types of expressions are as follows:
% cat expr_logger.txt | cut -d ' ' -f 2 | sort | uniq -c | sort -r -n
92277 NameExpr
38158 MemberExpr
25766 CallExpr
18001 StrExpr
10924 IntExpr
8503 TempNode
5161 EllipsisExpr
4837 ComparisonExpr
4648 OpExpr
4048 TupleExpr
3502 IndexExpr
2723 ListExpr
2683 UnaryExpr
1896 SliceExpr
478 ListComprehension
404 GeneratorExpr
367 DictExpr
332 ConditionalExpr
291 SuperExpr
256 TypeAliasExpr
240 SetExpr
173 LambdaExpr
169 YieldExpr
148 TypeVarExpr
101 CastExpr
40 SetComprehension
40 DictionaryComprehension
26 FloatExpr
22 NamedTupleExpr
20 BytesExpr
12 YieldFromExpr
2 TypedDictExpr
The only expression statements in Mypy are call expressions, which isn't surprising:
% cat stmt_logger.txt | cut -d ' ' -f 2 | sort | uniq -c | sort -r -n
409 CallExpr
I log the following data in those cases so we can dig deeper:
% head stmt_logger.txt
EXPR CallExpr -> Any in /Users/user/projects/mypy/mypy/test/data.py:201:8
EXPR CallExpr (None) -> Any in /Users/user/projects/mypy/mypy/test/data.py:217:12
EXPR CallExpr (None) -> Any in /Users/user/projects/mypy/mypy/test/data.py:219:8
EXPR CallExpr (None) -> Any in /Users/user/projects/mypy/mypy/test/data.py:221:12
EXPR CallExpr (shutil.copytree) -> builtins.str in /Users/user/projects/mypy/mypy/test/data.py:232:16
EXPR CallExpr (None) -> builtins.int in /Users/user/projects/mypy/mypy/test/data.py:245:16
EXPR CallExpr (None) -> Any in /Users/user/projects/mypy/mypy/test/data.py:269:12
EXPR CallExpr (None) -> builtins.str* in /Users/user/projects/mypy/mypy/test/data.py:396:8
EXPR CallExpr (None) -> Any in /Users/user/projects/mypy/mypy/test/data.py:488:4
EXPR CallExpr (None) -> Any in /Users/user/projects/mypy/mypy/test/data.py:491:4
Numbers by the function called:
% cat stmt_logger.txt | sed 's/.*(\(.*\)).*/\1/' | sort | uniq -c | sort -r -n
382 None
3 shutil.copyfile
3 mypy.stubtest.test_stubs
2 unittest.main
2 mypy.test.testdaemon.run_cmd
2 mypy.dmypy.client.get_status
2 mypy.dmypy.client.check_status
1 subprocess.check_output
1 shutil.copytree
1 shutil.copy
1 os.umask
1 mypy.test.testparse.skip
1 mypy.parse.parse
1 mypy.build.load_graph
1 mypy.build.find_module_and_diagnose
1 mypy.build.build
1 gc.collect
1 func
1 builtins.list
1 EXPR CallExpr -> Any in /Users/user/projects/mypy/mypy/test/data.py:201:8
This is clearly wrong because I fail to extract the full names of the majority of the callees here but it's not obvious to me why.
Numbers by the type returned (ignore the string-cutting artifacts in complex types containing whitespace, I just use cut here for simplicity):
% cat stmt_logger.txt | cut -d ' ' -f 5 | sort | uniq -c | sort -r -n
120 argparse.Action
73 builtins.int
41 builtins.bool
38 Any
24 mypy.types.Type
16 builtins.int*
6 builtins.list[builtins.str]
6 Tuple[builtins.int,
5 builtins.str
4 sqlite3.dbapi2.Cursor
4 mypy.types.Type*
4 builtins.set*[builtins.str]
4 builtins.bool*
4 Union[Literal['C'],
3 mypy.types.Instance*
3 mypy.nodes.FuncItem*
3 builtins.dict*[builtins.str,
3 Union[mypy.types.Type,
3 Tuple[mypy.types.Type,
3 Tuple[builtins.str,
2 unittest.TestProgram
2 typing.AbstractSet[builtins.str]
2 os.stat_result
2 mypy.types.TypeVarDef
2 mypy.plugin.Plugin*
2 mypy.nodes.IndexExpr*
2 mypy.binder.Frame
2 builtins.bytes
2 Union[mypy.nodes.TypeInfo,
2 Union[mypy.nodes.SymbolTable,
1 typed_ast.ast3.AST*
1 mypy.types.TypeAliasType*
1 mypy.split_namespace.SplitNamespace*
1 mypy.report.AbstractReporter
1 mypy.nodes.TypeInfo*
1 mypy.nodes.MypyFile
1 mypy.nodes.Expression*
1 mypy.moduleinspect.ModuleProperties
1 mypy.build.BuildResult
1 in
1 builtins.str*
1 builtins.list[mypy.types.Type]
1 builtins.list[mypy.types.TypeVarDef]
1 builtins.list[Union[mypy.build.BuildResult,
1 builtins.list*[builtins.str]
1 builtins.dict[builtins.str,
1 argparse.Namespace*
1 Union[mypy.types.Instance,
1 Union[builtins.str,
1 Union[builtins.int,
1 Tuple[mypy.types.TypeAliasType*,
1 Tuple[mypy.types.Instance*,
I can't comment on Mypy-specific types, but as for some others:
argparse.Actionis returned by all those argparse calls:
mypy/dmypy/client.py:57: error: Unused expression of type 'argparse.Action' [misc]
p.add_argument('--timeout', metavar='TIMEOUT', type=int,
^
mypy/dmypy/client.py:59: error: Unused expression of type 'argparse.Action' [misc]
p.add_argument('flags', metavar='FLAG', nargs='*', type=str,
^
mypy/dmypy/client.py:63: error: Unused expression of type 'argparse.Action' [misc]
p.add_argument('-v', '--verbose', action='store_true', help="Print detailed status")
^
mypy/dmypy/client.py:64: error: Unused expression of type 'argparse.Action' [misc]
p.add_argument('--fswatcher-dump-file', help="Collect information about the current file state")
^
mypy/dmypy/client.py:72: error: Unused expression of type 'argparse.Action' [misc]
p.add_argument('-v', '--verbose', action='store_true', help="Print detailed status")
^
mypy/dmypy/client.py:73: error: Unused expression of type 'argparse.Action' [misc]
p.add_argument('-q', '--quiet', action='store_true', help=argparse.SUPPRESS) # Deprecated
^
and since there are a lot of arguments there's a lot of those false-positives.
stderror.write(),file.write()and other similar calls:
mypy/main.py:78: error: Unused expression of type 'builtins.int' [misc]
f.write(msg + '\n')
^
mypy/main.py:117: error: Unused expression of type 'builtins.int' [misc]
stdout.write(formatter.format_error(n_errors, n_files, len(sources),
^
mypy/main.py:120: error: Unused expression of type 'builtins.int' [misc]
stdout.write(formatter.format_success(len(sources),
^
Those are almost entirely false-positives – all cases I looked at here were text tiles and TextIO.write() never performs partial writes so its return value is irrelevant.
__init__()methods returnAny(?)dict.pop()andlist.pop()return values but they're commonly used to just remove values from the collections
I'm attaching a full report of running modified Mypy on itself for your consideration. After looking at the output of this experiment I'm tempted to claim that instead of having an opt-in way to force use of values it makes more sense to have a flag to make it default and also provide an opt-out mechanism.
Edit: by an opt-out mechanism I mean something like:
_may_not_use_flag = object()
_T = TypeVar('_T')
MayNotUse = Annotated[_T, _may_not_use_flag]
# Always performs full write, the return value is not important unless one wants to
# count the bytes written
class TextIO:
def write(self, data: str) -> MayNotUse[int]:
# ...
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Hướng nghiên cứu
Bắt đầu bằng việc xem xét các commit thử nghiệm và báo cáo được liên kết, sau đó kiểm tra các chẩn đoán về câu lệnh biểu thức được hiển thị trong mypy/main.py, mypy/dmypy/client.py và test/data.py. Issue này không xác định một thiết kế đã được thống nhất hay các tiêu chí chấp nhận; để hoàn thành, cần xác lập ngữ nghĩa must-use và opt-out, đồng thời xác minh các chẩn đoán dự kiến.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- python
- Lĩnh vực
- devtools, tooling
- Loại issue
- Tính năng
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức độ hoạt động
- Đình trệ
- Độ rõ ràng
- Cần làm rõ
- Mức phù hợp với người mới
- 25/100