11"""Various command-line specific support."""
22
3+ import argparse
4+ import configparser
35import logging
6+ import os
47
58from pkgcore .util import commandline
9+ from snakeoil .cli import arghparse
10+ from snakeoil .contexts import patch
11+ from snakeoil .klass import jit_attr_none
12+ from snakeoil .mappings import OrderedSet
13+
14+ from . import const
615
716
817class Tool (commandline .Tool ):
@@ -11,3 +20,116 @@ def main(self):
1120 # suppress all pkgcore log messages
1221 logging .getLogger ('pkgcore' ).setLevel (100 )
1322 return super ().main ()
23+
24+
25+ class ConfigArg (argparse ._StoreAction ):
26+ """Store config path string or False when explicitly disabled."""
27+
28+ def __call__ (self , parser , namespace , values , option_string = None ):
29+ if values .lower () in ('false' , 'no' , 'n' ):
30+ values = False
31+ setattr (namespace , self .dest , values )
32+
33+
34+ class ConfigParser (configparser .ConfigParser ):
35+ """ConfigParser with case-sensitive keys (default forces lowercase)."""
36+
37+ def optionxform (self , option ):
38+ return option
39+
40+
41+ class ConfigFileParser :
42+ """Argument parser that supports loading settings from specified config files."""
43+
44+ default_configs = (const .SYSTEM_CONF_FILE , const .USER_CONF_FILE )
45+
46+ def __init__ (self , parser : arghparse .ArgumentParser , configs = (), ** kwargs ):
47+ super ().__init__ (** kwargs )
48+ self .parser = parser
49+ self .configs = OrderedSet (configs )
50+
51+ @jit_attr_none
52+ def config (self ):
53+ return self .parse_config ()
54+
55+ def parse_config (self , configs = ()):
56+ """Parse given config files."""
57+ configs = configs if configs else self .configs
58+ config = ConfigParser (default_section = None )
59+ try :
60+ for f in configs :
61+ config .read (f )
62+ except configparser .ParsingError as e :
63+ self .parser .error (f'parsing config file failed: { e } ' )
64+ return config
65+
66+ def parse_config_sections (self , namespace , sections ):
67+ """Parse options from a given iterable of config section names."""
68+ assert self .parser .prog .startswith ('pkgdev ' )
69+ module = self .parser .prog .split (' ' , 1 )[1 ] + '.'
70+ with patch ('snakeoil.cli.arghparse.ArgumentParser.error' , self ._config_error ):
71+ for section in (x for x in sections if x in self .config ):
72+ config_args = ((k .split ('.' , 1 )[1 ], v ) for k , v in self .config .items (section ) if k .startswith (module ))
73+ config_args = (f'--{ k } ={ v } ' if v else f'--{ k } ' for k , v in config_args )
74+ namespace , args = self .parser .parse_known_optionals (config_args , namespace )
75+ if args :
76+ self .parser .error (f"unknown arguments: { ' ' .join (args )} " )
77+ return namespace
78+
79+ def parse_config_options (self , namespace , configs = ()):
80+ """Parse options from config if they exist."""
81+ configs = list (filter (os .path .isfile , configs ))
82+ if not configs :
83+ return namespace
84+
85+ self .configs .update (configs )
86+ # reset jit attr to force reparse
87+ self ._config = None
88+
89+ # load default options
90+ namespace = self .parse_config_sections (namespace , ['DEFAULT' ])
91+
92+ return namespace
93+
94+ def _config_error (self , message , status = 2 ):
95+ """Stub to replace error method that notes config failure."""
96+ self .parser .exit (status , f'{ self .parser .prog } : failed loading config: { message } \n ' )
97+
98+ class ArgumentParser (arghparse .ArgumentParser ):
99+ """Parse all known arguments, from command line and config file."""
100+
101+ def __init__ (self , parents = (), ** kwargs ):
102+ self .config_argparser = arghparse .ArgumentParser (suppress = True )
103+ config_options = self .config_argparser .add_argument_group ('config options' )
104+ config_options .add_argument (
105+ '--config' , action = ConfigArg , dest = 'config_file' ,
106+ help = 'use custom pkgdev settings file' ,
107+ docs = """
108+ Load custom pkgdev scan settings from a given file.
109+
110+ Note that custom user settings override all other system and repo-level
111+ settings.
112+
113+ It's also possible to disable all types of settings loading by
114+ specifying an argument of 'false' or 'no'.
115+ """ )
116+ super ().__init__ (parents = [* parents , self .config_argparser ], ** kwargs )
117+
118+ def parse_known_args (self , args = None , namespace = None ):
119+ temp_namespace , _ = self .config_argparser .parse_known_args (args , namespace )
120+ # parser supporting config file options
121+ config_parser = ConfigFileParser (self )
122+ # always load settings from bundled config
123+ namespace = config_parser .parse_config_options (
124+ namespace , configs = [const .BUNDLED_CONF_FILE ])
125+
126+ # load default args from system/user configs if config-loading is allowed
127+ if temp_namespace .config_file is None :
128+ namespace = config_parser .parse_config_options (
129+ namespace , configs = ConfigFileParser .default_configs )
130+ elif temp_namespace .config_file is not False :
131+ namespace = config_parser .parse_config_options (
132+ namespace , configs = (namespace .config_file , ))
133+
134+ # parse command line args to override config defaults
135+ return super ().parse_known_args (args , namespace )
0 commit comments