python / python/mypy

An experiment in disallowing expression statements (or "unused values"), must_use/MustUse etc.

Aperta
#8,951 12 commenti 2 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Lingua principale
Python
Stelle
20.6k
Fork
3.3k
Metriche di merge delle PR
Metriche PR in attesa

Descrizione

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.Action is 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 return Any (?)
  • dict.pop() and list.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.

report.txt

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]:
        # ...

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia esaminando i commit sperimentali e il report collegati, quindi analizza le diagnostiche delle espressioni come istruzioni mostrate in mypy/main.py, mypy/dmypy/client.py e test/data.py. L’issue non definisce un design consolidato né criteri di accettazione; per completarla sarebbe necessario stabilire la semantica di must-use e opt-out e verificare le diagnostiche previste.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
python
Ambito
devtools, tooling
Tipo di issue
Funzionalità
Difficoltà
5/5
Tempo stimato
Più di una settimana
Stato di attività
Ferma
Chiarezza
Da chiarire
Idoneità per principianti
25/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.