Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1''' Module handling the parsing of FlashFlow configuration files. 

2 

3FlashFlow uses Python's standard :class:`configparser.ConfigParser` with 

4:class:`configparser.ExtendedInterpolation` to build a single config object. 

5The same object contains the :mod:`logging` configuration suitable for handing 

6off to Python via :func:`logging.config.fileConfig`. See :mod:`logging.config` 

7for information on the format of the logging config. 

8 

9Default options are loaded from :const:`DEF_CONF_INI` 

10(``flashflow/config.default.ini``), which is then extended with 

11:const:`DEF_CONF_LOG_INI` (``flashflow/config.log.default.ini``). 

12 

13It is good practice to fetch ints, floats, and bools from the config with 

14:meth:`configparser.ConfigParser.getint`, 

15:meth:`configparser.ConfigParser.getfloat`, and 

16:meth:`configparser.ConfigParser.getboolean` respectively. FlashFlow extends 

17:class:`ConfigParser` with two additional converters: 

18 

19 1. For file paths that automatically expands ``~`` and environment 

20 variables (*with two '$', not one*). See :meth:`expand_path`. 

21 2. For parsing a ``hostname:port`` string into a ``(str, int)`` 

22 tuple. Use ``conf.getpath(...)`` and ``conf.getaddr(...)`` for these. 

23 See :meth:`expand_addr`. 

24''' 

25from configparser import ConfigParser, ExtendedInterpolation 

26from tempfile import NamedTemporaryFile 

27import os 

28import logging 

29import logging.config 

30from typing import Optional, Tuple 

31 

32from . import PKG_DIR 

33 

34 

35log = logging.getLogger(__name__) 

36 

37DEF_CONF_INI = os.path.join(PKG_DIR, 'config.default.ini') 

38DEF_CONF_LOG_INI = os.path.join(PKG_DIR, 'config.log.default.ini') 

39 

40 

41def get_config(user_conf_fname: Optional[str]): 

42 ''' **THE** function to call in order to parse and receive the 

43 configuration that the user wants to use. 

44 

45 First gather the default options, then apply the config found in the given 

46 filename, if any. 

47 ''' 

48 conf = _get_default_config() 

49 conf = _get_default_log_config(conf=conf) 

50 conf = _get_user_config(user_conf_fname, conf=conf) 

51 return conf 

52 

53 

54def config_logging(conf): 

55 ''' Called near the very beginning of execution to finish configuring 

56 Python's :mod:`logging`. 

57 ''' 

58 ########################################################################### 

59 # Apply the user's actual desired path for per-second measurement results. 

60 # It's weird because the logging config system wants a 4-tuple of args ... 

61 # as a str. 

62 # arg 1: the filename 

63 os.makedirs(conf.getpath('coord', 'resultsdir'), mode=0o700, exist_ok=True) 

64 fname = conf.getpath('coord', 'results_log') 

65 # arg 2 and 3: the mode, the encoding 

66 mode, encoding = 'a', None 

67 # arg 4: whether to delay opening the file until first writing 

68 delay = True 

69 # now overwrite the config value 

70 conf['handler_results']['args'] = str((fname, mode, encoding, delay)) 

71 ########################################################################### 

72 # Write out the conf we are storing in memory to a temporary file, as the 

73 # file-based configuration of the logging system requires a file with a 

74 # filename. 

75 with NamedTemporaryFile('w+t') as fd: 

76 conf.write(fd) 

77 fd.seek(0, 0) 

78 logging.config.fileConfig(fd.name) 

79 

80 

81def _get_default_config(): 

82 conf = _empty_config() 

83 return _extend_config(conf, DEF_CONF_INI) 

84 

85 

86def _get_default_log_config(conf=None): 

87 conf = conf or _empty_config() 

88 return _extend_config(conf, DEF_CONF_LOG_INI) 

89 

90 

91def _get_user_config(fname: Optional[str], conf=None): 

92 conf = conf or _empty_config() 

93 if fname is None: 93 ↛ 95line 93 didn't jump to line 95, because the condition on line 93 was never false

94 return conf 

95 return _extend_config(conf, fname) 

96 

97 

98def _extend_config(conf, fname: str): 

99 # Logging here probably won't work. It probably hasn't been configured yet. 

100 # print(fname) 

101 with open(fname, 'rt') as fd: 

102 conf.read_file(fd, source=fname) 

103 return conf 

104 

105 

106def expand_path(path: str) -> str: 

107 ''' Expand path string containing shell variables and ``~`` into their 

108 values. 

109 

110 Environment variables must have their ``$`` escaped by another ``$``. 

111 For example, ``$$XDG_RUNTIME_DIR/foo.bar``. 

112 

113 This function is only public so it gets documented. It is not intended to 

114 be used outside of this module. 

115 ''' 

116 return os.path.expanduser(os.path.expandvars(path)) 

117 

118 

119def expand_addr(addr: str) -> Optional[Tuple[str, int]]: 

120 ''' Parse the given string into a (hostname, port) tuple. 

121 

122 Not much effort is put into validation: 

123 - the port is checked to be a valid integer 

124 - if the host looks like an ipv6 address with brackets, they are 

125 removed 

126 

127 Otherwise the values are left as-is. 

128 

129 On success, returns (hostname, port) where port is an integer. On error, 

130 logs about the error and returns None. ConfigParser does **not** see this 

131 an error case worthy of special treatement, so you need to check if the 

132 returned value is None yourself. 

133 

134 :: 

135 

136 '127.0.0.1' --> None (error: no port) 

137 ':1234' --> None (error: no host) 

138 'example.com:asdf' --> None (error: invalid port) 

139 'localhost:1234' --> ('localhost', 1234) 

140 ':1234' --> ('', 1234) 

141 '127.0.0.1:1234' --> ('127.0.0.1', 1234) 

142 '[::1]:0' --> ('::1', 0) 

143 '::1:0' --> ('::1', 0) 

144 

145 It's not up to this function to decide how to specify "listen on all hosts" 

146 or "pick a port for me." These things should be documented and decided 

147 elsewhere. 

148 

149 This function is only public so it gets documented. It is not intended to 

150 be used outside of this module. 

151 ''' 

152 try: 

153 a, p = addr.rsplit(':', 1) 

154 if a.startswith('[') and a.endswith(']'): 154 ↛ 155line 154 didn't jump to line 155, because the condition on line 154 was never true

155 a = a[1:-1] 

156 return a, int(p) 

157 except ValueError as e: 

158 log.error('invalid host:port "%s" : %s', addr, e) 

159 return None 

160 

161 

162def _empty_config() -> ConfigParser: 

163 return ConfigParser( 

164 interpolation=ExtendedInterpolation(), 

165 converters={ 

166 'path': expand_path, 

167 'addr': expand_addr, 

168 })