1919import errno
2020import io
2121import os
22+ import sys
23+ from typing import Any
24+ from typing import TypedDict
2225
2326try :
2427 # Python 3.11+
2831 import tomli as tomllib # type: ignore
2932
3033
31- def _read_pyproject_toml (root ):
34+ def _read_raw (root : str , filename : str ) -> str | None :
35+ try :
36+ path = os .path .join (root , filename )
37+ with open (path , encoding = "utf-8" ) as f :
38+ data = f .read ()
39+ if not data .endswith ('\n ' ):
40+ print (
41+ f"Requirements file { filename } does not end with a "
42+ f"newline." ,
43+ file = sys .stderr ,
44+ )
45+ return data
46+ except OSError as e :
47+ if e .errno == errno .ENOENT :
48+ return None
49+
50+ raise
51+
52+
53+ def _read_pyproject_toml (root : str ) -> dict [str , Any ] | None :
3254 data = _read_raw (root , 'pyproject.toml' )
3355 if data is None :
3456 return None
3557
3658 return tomllib .loads (data )
3759
3860
39- def _read_pyproject_toml_requirements (root ):
61+ def _read_requirements_txt (root : str , filename : str ) -> list [str ] | None :
62+ data = _read_raw (root , filename )
63+ if data is None :
64+ return None
65+
66+ result = []
67+ for line in data .splitlines ():
68+ # we only ignore comments and empty lines: everything else is
69+ # handled later
70+ line = line .strip ()
71+
72+ if line .startswith ('#' ) or not line :
73+ continue
74+
75+ result .append (line )
76+
77+ return result
78+
79+
80+ def _read_pyproject_toml_requirements (root : str ) -> list [str ] | None :
4081 data = _read_pyproject_toml (root ) or {}
4182
4283 # projects may not have PEP-621 project metadata
4384 if 'project' not in data :
4485 return None
4586
46- # FIXME(stephenfin): We should not be doing this, but the fix requires a
47- # larger change to do normalization here.
48- return '\n ' .join (data ['project' ].get ('dependencies' , []))
87+ return data ['project' ].get ('dependencies' , [])
4988
5089
51- def _read_pyproject_toml_extras (root ) :
90+ def _read_pyproject_toml_extras (root : str ) -> dict [ str , list [ str ]] | None :
5291 data = _read_pyproject_toml (root ) or {}
5392
5493 # projects may not have PEP-621 project metadata
5594 if 'project' not in data :
5695 return None
5796
58- # FIXME(stephenfin): As above, we should not be doing this.
59- return {
60- k : '\n ' .join (v ) for k , v in
61- data ['project' ].get ('optional-dependencies' , {}).items ()
62- }
97+ return data ['project' ].get ('optional-dependencies' , {})
6398
6499
65- def _read_setup_cfg_extras (root ) :
100+ def _read_setup_cfg_extras (root : str ) -> dict [ str , list [ str ]] | None :
66101 data = _read_raw (root , 'setup.cfg' )
67102 if data is None :
68103 return None
@@ -72,20 +107,32 @@ def _read_setup_cfg_extras(root):
72107 if not c .has_section ('extras' ):
73108 return None
74109
75- return dict (c .items ('extras' ))
110+ result : dict [str , list [str ]] = {}
111+ for extra , deps in c .items ('extras' ):
112+ result [extra ] = []
113+ for line in deps .splitlines ():
114+ # we only ignore comments and empty lines: everything else is
115+ # handled later
116+ line = line .strip ()
76117
118+ if line .startswith ('#' ) or not line :
119+ continue
77120
78- def _read_raw (root , filename ):
79- try :
80- path = os .path .join (root , filename )
81- with open (path , encoding = "utf-8" ) as f :
82- return f .read ()
83- except OSError as e :
84- if e .errno != errno .ENOENT :
85- raise
121+ result [extra ].append (line )
122+
123+ return result
124+
125+
126+ class Project (TypedDict ):
127+ # The root directory path
128+ root : str
129+ # A mapping of filename to the contents of that file
130+ requirements : dict [str , list [str ]]
131+ # A mapping of filename to extras from that file
132+ extras : dict [str , dict [str , list [str ]]]
86133
87134
88- def read (root ) :
135+ def read (root : str ) -> Project :
89136 """Read into memory the packaging data for the project at root.
90137
91138 :param root: A directory path.
@@ -96,11 +143,13 @@ def read(root):
96143 requirements
97144 """
98145 # Store root directory and installer-related files for later processing
99- result = {'root' : root }
146+ result : Project = {
147+ 'root' : root ,
148+ 'requirements' : {},
149+ 'extras' : {},
150+ }
100151
101152 # Store requirements
102- result ['requirements' ] = {}
103-
104153 if (data := _read_pyproject_toml_requirements (root )) is not None :
105154 result ['requirements' ]['pyproject.toml' ] = data
106155
@@ -116,12 +165,10 @@ def read(root):
116165 'test-requirements-py2.txt' ,
117166 'test-requirements-py3.txt' ,
118167 ]:
119- if (data := _read_raw (root , filename )) is not None :
168+ if (data := _read_requirements_txt (root , filename )) is not None :
120169 result ['requirements' ][filename ] = data
121170
122171 # Store extras
123- result ['extras' ] = {}
124-
125172 if (data := _read_setup_cfg_extras (root )) is not None :
126173 result ['extras' ]['setup.cfg' ] = data
127174
0 commit comments