2
2
Check that test suite file doesn't use the pandas namespace inconsistently.
3
3
4
4
We check for cases of ``Series`` and ``pd.Series`` appearing in the same file
5
- (likewise for some other common classes ).
5
+ (likewise for other pandas objects ).
6
6
7
7
This is meant to be run as a pre-commit hook - to run it manually, you can do:
8
8
15
15
though note that you may need to manually fixup some imports and that you will also
16
16
need the additional dependency `tokenize-rt` (which is left out from the pre-commit
17
17
hook so that it uses the same virtualenv as the other local ones).
18
+
19
+ The general structure is similar to that of some plugins from
20
+ https://github.com/asottile/pyupgrade .
18
21
"""
19
22
20
23
import argparse
21
24
import ast
25
+ import sys
22
26
from typing import (
23
27
MutableMapping ,
28
+ NamedTuple ,
24
29
Optional ,
25
30
Sequence ,
26
31
Set ,
27
- Tuple ,
28
32
)
29
33
30
- ERROR_MESSAGE = "Found both `pd.{name}` and `{name}` in {path}"
31
- EXCLUDE = {
32
- "eval" , # built-in, different from `pd.eval`
33
- "np" , # pd.np is deprecated but still tested
34
- }
35
- Offset = Tuple [int , int ]
34
+ ERROR_MESSAGE = (
35
+ "{path}:{lineno}:{col_offset}: "
36
+ "Found both '{prefix}.{name}' and '{name}' in {path}"
37
+ )
38
+
39
+
40
+ class OffsetWithNamespace (NamedTuple ):
41
+ lineno : int
42
+ col_offset : int
43
+ namespace : str
36
44
37
45
38
46
class Visitor (ast .NodeVisitor ):
39
47
def __init__ (self ) -> None :
40
- self .pandas_namespace : MutableMapping [Offset , str ] = {}
41
- self .no_namespace : Set [str ] = set ()
48
+ self .pandas_namespace : MutableMapping [OffsetWithNamespace , str ] = {}
49
+ self .imported_from_pandas : Set [str ] = set ()
42
50
43
51
def visit_Attribute (self , node : ast .Attribute ) -> None :
44
- if (
45
- isinstance (node .value , ast .Name )
46
- and node .value .id == "pd"
47
- and node .attr not in EXCLUDE
48
- ):
49
- self .pandas_namespace [(node .lineno , node .col_offset )] = node .attr
52
+ if isinstance (node .value , ast .Name ) and node .value .id in {"pandas" , "pd" }:
53
+ offset_with_namespace = OffsetWithNamespace (
54
+ node .lineno , node .col_offset , node .value .id
55
+ )
56
+ self .pandas_namespace [offset_with_namespace ] = node .attr
50
57
self .generic_visit (node )
51
58
52
- def visit_Name (self , node : ast .Name ) -> None :
53
- if node .id not in EXCLUDE :
54
- self .no_namespace . add ( node .id )
59
+ def visit_ImportFrom (self , node : ast .ImportFrom ) -> None :
60
+ if node .module is not None and "pandas" in node . module :
61
+ self .imported_from_pandas . update ( name . name for name in node .names )
55
62
self .generic_visit (node )
56
63
57
64
@@ -64,9 +71,11 @@ def replace_inconsistent_pandas_namespace(visitor: Visitor, content: str) -> str
64
71
65
72
tokens = src_to_tokens (content )
66
73
for n , i in reversed_enumerate (tokens ):
74
+ offset_with_namespace = OffsetWithNamespace (i .offset [0 ], i .offset [1 ], i .src )
67
75
if (
68
- i .offset in visitor .pandas_namespace
69
- and visitor .pandas_namespace [i .offset ] in visitor .no_namespace
76
+ offset_with_namespace in visitor .pandas_namespace
77
+ and visitor .pandas_namespace [offset_with_namespace ]
78
+ in visitor .imported_from_pandas
70
79
):
71
80
# Replace `pd`
72
81
tokens [n ] = i ._replace (src = "" )
@@ -85,16 +94,28 @@ def check_for_inconsistent_pandas_namespace(
85
94
visitor = Visitor ()
86
95
visitor .visit (tree )
87
96
88
- inconsistencies = visitor .no_namespace .intersection (
97
+ inconsistencies = visitor .imported_from_pandas .intersection (
89
98
visitor .pandas_namespace .values ()
90
99
)
100
+
91
101
if not inconsistencies :
92
102
# No inconsistent namespace usage, nothing to replace.
93
- return content
103
+ return None
94
104
95
105
if not replace :
96
- msg = ERROR_MESSAGE .format (name = inconsistencies .pop (), path = path )
97
- raise RuntimeError (msg )
106
+ inconsistency = inconsistencies .pop ()
107
+ lineno , col_offset , prefix = next (
108
+ key for key , val in visitor .pandas_namespace .items () if val == inconsistency
109
+ )
110
+ msg = ERROR_MESSAGE .format (
111
+ lineno = lineno ,
112
+ col_offset = col_offset ,
113
+ prefix = prefix ,
114
+ name = inconsistency ,
115
+ path = path ,
116
+ )
117
+ sys .stdout .write (msg )
118
+ sys .exit (1 )
98
119
99
120
return replace_inconsistent_pandas_namespace (visitor , content )
100
121
0 commit comments