Coverage for /var/devmt/py/utils4_1.7.0/utils4/dict2obj.py: 100%

54 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-21 17:18 +0000

1# -*- coding: utf-8 -*- 

2""" 

3:Purpose: This class module is used to convert a Python dictionary (``dict``) 

4 or JSON file into a object - where the dictionary's key/value 

5 pairs become object attribute/value pairs. 

6 

7:Platform: Linux/Windows | Python 3.7+ 

8:Developer: J Berendt 

9:Email: support@s3dev.uk 

10 

11:Comments: Basic concept `attribution`_. 

12 

13 

14.. _attribution: https://stackoverflow.com/a/1639197/6340496 

15 

16""" 

17# pylint: disable=too-few-public-methods 

18 

19import os 

20import json 

21from string import punctuation 

22 

23 

24class Dict2Obj: 

25 """Create a Python object from a standard Python dictionary, or JSON file. 

26 

27 Args: 

28 dictionary (dict, optional): A standard Python dictionary where all 

29 key/value pairs will be converted into an object. Defaults to None. 

30 source (str, optional): Source for the conversion. Defaults to 'dict'. 

31 

32 - 'dict': a standard Python dictionary 

33 - 'json': uses content from a JSON file 

34 

35 filepath (str, optional): Full file path to the JSON file to be used. 

36 Defaults to None. 

37 

38 :Design: 

39 A Python object is created from the passed dictionary (or JSON 

40 file), where each of the dictionary's key/value pairs is turned 

41 into an object attribute/value pair. 

42 

43 :Note: 

44 

45 #. The dictionary or JSON file *should* be in a flat format. If a 

46 nested source is provided, the value of the object will be the 

47 nested structure. In other the object will *not* be nested. 

48 #. This can be useful when loading a JSON config file into 

49 memory, as you can then access it like an object, rather than a 

50 dictionary. 

51 

52 :Example: 

53 To convert a dictionary into an object:: 

54 

55 >>> from utils4.dict2obj import Dict2Obj 

56 

57 >>> d = dict(a=1, b=2, title='This is a title.') 

58 >>> obj = Dict2Obj(dictionary=d) 

59 >>> print(obj.title) 

60 

61 This is a title. 

62 

63 """ 

64 

65 _VALID = ['dict', 'json'] 

66 

67 def __init__(self, *, dictionary=None, source='dict', filepath=None): 

68 """Class initialiser.""" 

69 self._dict = dictionary 

70 self._src = source 

71 self._fpath = filepath 

72 self._create() 

73 

74 def _create(self): 

75 """Validate and create the object. 

76 

77 Raises: 

78 TypeError: If a key is not a string, or is a string yet begins 

79 with any type other than a string.. 

80 

81 """ 

82 if self._validate(): 

83 if self._src.lower() == 'json': 

84 # Read from json file. 

85 dict_ = self._read_json() 

86 else: 

87 # Create object from passed dictionary. 

88 dict_ = self._dict 

89 # Create replacement translation. 

90 trans = str.maketrans({p: '' for p in punctuation}) 

91 trans.update({32: '_'}) 

92 # Loop through the dict and set class attributes. 

93 for k, v in dict_.items(): 

94 if isinstance(k, str) & (not str(k)[0].isdigit()): 

95 k = k.translate(trans) 

96 setattr(self, k, v) 

97 else: 

98 raise TypeError(f'Key error, string expected. Received {type(k)} for key: {k}.') 

99 

100 def _read_json(self) -> dict: 

101 """Read values from a JSON file into a dictionary. 

102 

103 Returns: 

104 dict: A dictionary containing the JSON data. 

105 

106 """ 

107 with open(self._fpath, 'r', encoding='utf-8') as f: 

108 return json.loads(f.read()) 

109 

110 def _validate(self) -> bool: 

111 """Run the following validation tests: 

112 

113 - The ``source`` value is valid. 

114 - If 'json' source, a file path is provided. 

115 - If 'json' source, the provided file path exists. 

116 

117 Returns: 

118 bool: True if **all** tests pass, otherwise False. 

119 

120 """ 

121 # pylint: disable=multiple-statements 

122 s = self._validate_source_value() 

123 if s: s = self._validate_source() 

124 if s: s = self._validate_is_dict() 

125 if s: s = self._validate_fileexists() 

126 return s 

127 

128 def _validate_fileexists(self) -> bool: 

129 """Validation test: If a 'json' source, test the file path exists. 

130 

131 Raises: 

132 ValueError: If the passed filepath is not a '.json' extension. 

133 ValueError: If the passed filepath does not exist. 

134 

135 Returns: 

136 bool: True if the source is 'dict'; or if source is 'json' and 

137 the file exists, otherwise False. 

138 

139 """ 

140 success = False 

141 if self._src.lower() == 'json': 

142 if os.path.exists(self._fpath): 

143 if os.path.splitext(self._fpath)[1].lower() == '.json': 

144 success = True 

145 else: 

146 raise ValueError(f'The file provided must be a JSON file:\n- {self._fpath}') 

147 else: 

148 raise ValueError(f'The file provided does not exist:\n- {self._fpath}') 

149 else: 

150 success = True 

151 return success 

152 

153 def _validate_is_dict(self) -> bool: 

154 """Validation test: Verify the object is a ``dict``. 

155 

156 Raises: 

157 TypeError: If the passed object is not a ``dict``. 

158 

159 Returns: 

160 bool: True if the passed object is a ``dict``. 

161 

162 """ 

163 if self._src == 'dict': 

164 if not isinstance(self._dict, dict): 

165 raise TypeError(f'Unexpected type. Expected a dict, received a {type(self._dict)}.') 

166 return True 

167 

168 def _validate_source(self) -> bool: 

169 """Validation test: If a 'json' source, test a file path is provided. 

170 

171 Raises: 

172 ValueError: If the source is 'json' and a filepath is not provided. 

173 

174 Returns: 

175 bool: True if the source is 'dict'; or if source is 'json' and a 

176 file path is provided. 

177 

178 """ 

179 if all([self._src.lower() == 'json', not self._fpath]): 

180 raise ValueError('A file path must be provided for the JSON file.') 

181 return True 

182 

183 def _validate_source_value(self) -> bool: 

184 """Validation test: The value of the ``source`` parameter is valid. 

185 

186 Raises: 

187 ValueError: If the source string is invalid. 

188 

189 Returns: 

190 bool: True if a valid source. 

191 

192 """ 

193 if self._src not in self._VALID: 

194 raise ValueError(f'The source provided ({self._src}) is invalid. ' 

195 f'Valid options are: {self._VALID}') 

196 return True