1+ from .logging import logger
12from configparser import ConfigParser
23from configparser import ExtendedInterpolation
34from pathlib import Path
45from urllib import parse
56from urllib import request
7+ from urllib .error import HTTPError
68
79import os
810import tempfile
@@ -12,14 +14,14 @@ def resolve_dependencies(
1214 file_or_url : str | Path ,
1315 tmpdir : str ,
1416 http_parent = None ,
17+ optional : bool = False ,
1518) -> list [Path ]:
1619 """Resolve dependencies of a file or url
1720
18- The result is a list of Path objects, starting with the
19- given file_or_url and followed by all file_or_urls referenced from it.
21+ Return included paths before their parent, so parent settings take precedence.
2022
21- The file_or_url is assumed to be a ini file or url to such, with an option key "include"
22- under the "[settings]" section .
23+ Follow "include" and "include-optional" under the "[settings]" section.
24+ If optional, skip a missing file or HTTP 404 for this input only .
2325 """
2426 if isinstance (file_or_url , str ):
2527 if http_parent :
@@ -29,40 +31,59 @@ def resolve_dependencies(
2931 # Windows drive letters are single characters, URL schemes are longer
3032 is_url = parsed .scheme and len (parsed .scheme ) > 1
3133 if is_url :
32- with request .urlopen (str (file_or_url )) as fio :
33- tf = tempfile .NamedTemporaryFile (
34- suffix = ".ini" ,
35- dir = str (tmpdir ),
36- delete = False ,
37- )
38- tf .write (fio .read ())
39- tf .flush ()
40- file = Path (tf .name )
41- parts = list (parsed )
42- parts [2 ] = str (Path (parts [2 ]).parent )
43- http_parent = parse .urlunparse (parts )
34+ try :
35+ with request .urlopen (str (file_or_url )) as fio :
36+ with tempfile .NamedTemporaryFile (
37+ suffix = ".ini" ,
38+ dir = str (tmpdir ),
39+ delete = False ,
40+ ) as tf :
41+ tf .write (fio .read ())
42+ file = Path (tf .name )
43+ except HTTPError as e :
44+ if optional and e .code == 404 :
45+ logger .info ("Skipping missing optional include: %s" , file_or_url )
46+ return []
47+ logger .error ("Error %s for URL: %s" , e .code , e .url )
48+ raise
49+ http_parent = parse .urljoin (str (file_or_url ), "." )
4450 else :
4551 file = Path (file_or_url )
4652 else :
4753 file = file_or_url
48- if not file .exists ():
49- raise FileNotFoundError (file )
5054 cfg = ConfigParser ()
51- cfg .read (file )
52- if not ("settings" in cfg and "include" in cfg ["settings" ]):
55+ try :
56+ with file .open () as fio :
57+ cfg .read_file (fio )
58+ except FileNotFoundError :
59+ if not optional :
60+ raise
61+ logger .info ("Skipping missing optional include: %s" , file_or_url )
62+ return []
63+ if "settings" not in cfg :
5364 return [file ]
5465 file_list = []
55- for include in cfg ["settings" ]["include" ].split ("\n " ):
56- include = include .strip ()
57- if not include :
58- continue
59- # Check if it's a real URL scheme (not a Windows drive letter)
60- parsed_include = parse .urlparse (include )
61- is_include_url = parsed_include .scheme and len (parsed_include .scheme ) > 1
62- if http_parent or is_include_url :
63- file_list += resolve_dependencies (include , tmpdir , http_parent )
64- else :
65- file_list += resolve_dependencies (file .parent / include , tmpdir )
66+ for directive in ("include" , "include-optional" ):
67+ for include in cfg ["settings" ].get (directive , "" ).splitlines ():
68+ include = include .strip ()
69+ if not include :
70+ continue
71+ # Check if it's a real URL scheme (not a Windows drive letter)
72+ parsed_include = parse .urlparse (include )
73+ is_include_url = parsed_include .scheme and len (parsed_include .scheme ) > 1
74+ if http_parent or is_include_url :
75+ file_list += resolve_dependencies (
76+ file_or_url = include ,
77+ tmpdir = tmpdir ,
78+ http_parent = http_parent ,
79+ optional = directive == "include-optional" ,
80+ )
81+ else :
82+ file_list += resolve_dependencies (
83+ file_or_url = file .parent / include ,
84+ tmpdir = tmpdir ,
85+ optional = directive == "include-optional" ,
86+ )
6687
6788 file_list .append (file )
6889 return file_list
@@ -80,6 +101,9 @@ def read_with_included(file_or_url: str | Path) -> ConfigParser:
80101 cfg .optionxform = str # type: ignore
81102 cfg ["settings" ]["directory" ] = os .getcwd ()
82103 with tempfile .TemporaryDirectory () as tmpdir :
83- resolved = resolve_dependencies (file_or_url , tmpdir )
104+ resolved = resolve_dependencies (
105+ file_or_url = file_or_url ,
106+ tmpdir = tmpdir ,
107+ )
84108 cfg .read (resolved )
85109 return cfg
0 commit comments