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

54 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-12 15:38 +0100

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.6+ 

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 s = self._validate_source_value() 

122 if s: s = self._validate_source() 

123 if s: s = self._validate_is_dict() 

124 if s: s = self._validate_fileexists() 

125 return s 

126 

127 def _validate_fileexists(self) -> bool: 

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

129 

130 Raises: 

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

132 ValueError: If the passed filepath does not exist. 

133 

134 Returns: 

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

136 the file exists, otherwise False. 

137 

138 """ 

139 success = False 

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

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

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

143 success = True 

144 else: 

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

146 else: 

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

148 else: 

149 success = True 

150 return success 

151 

152 def _validate_is_dict(self) -> bool: 

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

154 

155 Raises: 

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

157 

158 Returns: 

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

160 

161 """ 

162 if self._src == 'dict': 

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

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

165 return True 

166 

167 def _validate_source(self) -> bool: 

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

169 

170 Raises: 

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

172 

173 Returns: 

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

175 file path is provided. 

176 

177 """ 

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

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

180 return True 

181 

182 def _validate_source_value(self) -> bool: 

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

184 

185 Raises: 

186 ValueError: If the source string is invalid. 

187 

188 Returns: 

189 bool: True if a valid source. 

190 

191 """ 

192 if self._src not in self._VALID: 

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

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

195 return True