66import os
77import pathlib
88import sys
9- from typing import Optional , Sequence
9+ from typing import Dict , List , Optional , Sequence , Tuple , Union
1010
1111LIB_ROOT = pathlib .Path (__file__ ).parent / "lib" / "python"
1212sys .path .insert (0 , os .fspath (LIB_ROOT ))
1313
14+ import tomli
1415from importlib_metadata import metadata
1516from packaging .requirements import Requirement
1617
18+ DEFAULT_SEVERITY = 3
19+
1720
1821def parse_args (argv : Optional [Sequence [str ]] = None ):
1922 if argv is None :
2023 argv = sys .argv [1 :]
2124 parser = argparse .ArgumentParser (
2225 description = "Check for installed packages against requirements"
2326 )
24- parser .add_argument (
25- "REQUIREMENTS" , type = str , help = "Path to requirements.[txt, in]" , nargs = "+"
26- )
27+ parser .add_argument ("FILEPATH" , type = str , help = "Path to requirements.[txt, in]" )
2728
2829 return parser .parse_args (argv )
2930
@@ -39,32 +40,88 @@ def parse_requirements(line: str) -> Optional[Requirement]:
3940 return None
4041
4142
42- def main ():
43- args = parse_args ()
43+ def process_requirements (req_file : pathlib .Path ) -> List [Dict [str , Union [str , int ]]]:
44+ diagnostics = []
45+ for n , line in enumerate (req_file .read_text (encoding = "utf-8" ).splitlines ()):
46+ if line .startswith (("#" , "-" , " " )) or line == "" :
47+ continue
48+
49+ req = parse_requirements (line )
50+ if req :
51+ try :
52+ # Check if package is installed
53+ metadata (req .name )
54+ except :
55+ diagnostics .append (
56+ {
57+ "line" : n ,
58+ "character" : 0 ,
59+ "endLine" : n ,
60+ "endCharacter" : len (req .name ),
61+ "package" : req .name ,
62+ "code" : "not-installed" ,
63+ "severity" : DEFAULT_SEVERITY ,
64+ }
65+ )
66+ return diagnostics
4467
68+
69+ def get_pos (lines : List [str ], text : str ) -> Tuple [int , int , int , int ]:
70+ for n , line in enumerate (lines ):
71+ index = line .find (text )
72+ if index >= 0 :
73+ return n , index , n , index + len (text )
74+ return (0 , 0 , 0 , 0 )
75+
76+
77+ def process_pyproject (req_file : pathlib .Path ) -> List [Dict [str , Union [str , int ]]]:
4578 diagnostics = []
46- for req_file in args .REQUIREMENTS :
47- req_file = pathlib .Path (req_file )
48- if req_file .exists ():
49- lines = req_file .read_text (encoding = "utf-8" ).splitlines ()
50- for n , line in enumerate (lines ):
51- if line .startswith (("#" , "-" , " " )) or line == "" :
52- continue
53-
54- req = parse_requirements (line )
55- if req :
56- try :
57- # Check if package is installed
58- metadata (req .name )
59- except :
60- diagnostics .append (
61- {
62- "line" : n ,
63- "package" : req .name ,
64- "code" : "not-installed" ,
65- "severity" : 3 ,
66- }
67- )
79+ try :
80+ raw_text = req_file .read_text (encoding = "utf-8" )
81+ pyproject = tomli .loads (raw_text )
82+ except :
83+ return diagnostics
84+
85+ lines = raw_text .splitlines ()
86+ reqs = pyproject .get ("project" , {}).get ("dependencies" , [])
87+ for raw_req in reqs :
88+ req = parse_requirements (raw_req )
89+ n , start , _ , end = get_pos (lines , raw_req )
90+ if req :
91+ try :
92+ # Check if package is installed
93+ metadata (req .name )
94+ except :
95+ diagnostics .append (
96+ {
97+ "line" : n ,
98+ "character" : start ,
99+ "endLine" : n ,
100+ "endCharacter" : end ,
101+ "package" : req .name ,
102+ "code" : "not-installed" ,
103+ "severity" : DEFAULT_SEVERITY ,
104+ }
105+ )
106+ return diagnostics
107+
108+
109+ def get_diagnostics (req_file : pathlib .Path ) -> List [Dict [str , Union [str , int ]]]:
110+ diagnostics = []
111+ if not req_file .exists ():
112+ return diagnostics
113+
114+ if req_file .name == "pyproject.toml" :
115+ diagnostics = process_pyproject (req_file )
116+ else :
117+ diagnostics = process_requirements (req_file )
118+
119+ return diagnostics
120+
121+
122+ def main ():
123+ args = parse_args ()
124+ diagnostics = get_diagnostics (pathlib .Path (args .FILEPATH ))
68125 print (json .dumps (diagnostics , ensure_ascii = False ))
69126
70127
0 commit comments