1+ import ast
12import logging
2- import subprocess
33from argparse import Namespace
44
5- from same_version .extractors .file_extractor import FileExtractor
5+ from same_version .extractors .py_ast_extractor import PyAstExtractor
66
77logger = logging .getLogger (__name__ )
88
9- class SetupPyExtractor (FileExtractor ):
9+ class SetupPyExtractor (PyAstExtractor ):
1010
1111 def __init__ (self , cli_args : Namespace ):
1212 target_cli_parameter_name : str = '--setup-py-path'
@@ -17,21 +17,62 @@ def __init__(self, cli_args: Namespace):
1717 target_cli_parameter_name = target_cli_parameter_name
1818 )
1919
20- def _get_data (self ) -> dict :
21- data = {}
22-
23- if not self .target_exists :
24- data ['version' ] = None
25- else :
26- try :
27- output = subprocess .check_output (['python' , str (self .target_file_path ), '--version' ], stderr = subprocess .STDOUT )
28- data ['version' ] = output .decode ('utf-8' ).strip ()
29- except subprocess .CalledProcessError as e :
30- logger .error (f"❌ Error running { self .target_name } --version:\n { e .output .decode ('utf-8' )} " )
31- data ['version' ] = None
32-
33- return data
34-
3520 def _get_version_from_data (self , data : dict ) -> str | None :
36- version = data .get ('version' , None )
37- return version
21+ tree = data .get ('tree' , None )
22+ if tree is None :
23+ return None
24+
25+ visitor = SetupPyVisitor ()
26+ visitor .visit (tree )
27+
28+ version : str | None = visitor .version
29+ if not isinstance (version , str ) and version is not None :
30+ version = None
31+
32+ return version
33+
34+
35+ class SetupPyVisitor (ast .NodeVisitor ):
36+ def __init__ (self ):
37+ self .version = None
38+ self .assignments = {}
39+
40+ def visit_Assign (self , node ):
41+ # Capture simple assignments: version = "1.2.3"
42+ if len (node .targets ) == 1 and isinstance (node .targets [0 ], ast .Name ):
43+ var_name = node .targets [0 ].id
44+ value = self ._get_constant_value (node .value )
45+ if value is not None :
46+ self .assignments [var_name ] = value
47+
48+ def visit_Call (self , node ):
49+ # Look for any call to 'setup' function
50+ if self ._is_setup_call (node ):
51+ for kw in node .keywords :
52+ if kw .arg == "version" :
53+ value = self ._get_constant_value (kw .value )
54+ if value is None and isinstance (kw .value , ast .Name ):
55+ # Handle: version=version_var
56+ value = self .assignments .get (kw .value .id )
57+ if value is not None :
58+ self .version = value
59+
60+ def _get_constant_value (self , node ):
61+ if isinstance (node , ast .Constant ): # Python 3.8+
62+ if isinstance (node .value , str ):
63+ return node .value
64+ elif isinstance (node , ast .Str ): # Python <3.8
65+ return node .s
66+ return None
67+
68+ def _is_setup_call (self , node ):
69+ # Accept setup() or setuptools.setup()
70+ if isinstance (node .func , ast .Name ):
71+ return node .func .id == "setup"
72+ if isinstance (node .func , ast .Attribute ):
73+ return (
74+ node .func .attr == "setup" and
75+ isinstance (node .func .value , ast .Name ) and
76+ node .func .value .id == "setuptools"
77+ )
78+ return False
0 commit comments