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

54 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2025-08-26 21:39 +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:Developer: J Berendt 

8:Email: support@s3dev.uk 

9 

10:Comments: Basic concept `attribution`_. 

11 

12 

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

14 

15""" 

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

17 

18import os 

19import json 

20from string import punctuation 

21 

22 

23class Dict2Obj: 

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

25 

26 Args: 

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

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

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

30 

31 - 'dict': a standard Python dictionary 

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

33 

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

35 Defaults to None. 

36 

37 :Design: 

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

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

40 into an object attribute/value pair. 

41 

42 :Note: 

43 

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

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

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

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

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

49 dictionary. 

50 

51 :Example: 

52 To convert a dictionary into an object:: 

53 

54 >>> from utils4.dict2obj import Dict2Obj 

55 

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

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

58 >>> print(obj.title) 

59 

60 This is a title. 

61 

62 """ 

63 

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

65 

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

67 """Class initialiser.""" 

68 self._dict = dictionary 

69 self._src = source 

70 self._fpath = filepath 

71 self._create() 

72 

73 def _create(self): 

74 """Validate and create the object. 

75 

76 Raises: 

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

78 with any type other than a string.. 

79 

80 """ 

81 if self._validate(): 

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

83 # Read from json file. 

84 dict_ = self._read_json() 

85 else: 

86 # Create object from passed dictionary. 

87 dict_ = self._dict 

88 # Create replacement translation. 

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

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

91 # Loop through the dict and set class attributes. 

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

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

94 k = k.translate(trans) 

95 setattr(self, k, v) 

96 else: 

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

98 

99 def _read_json(self) -> dict: 

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

101 

102 Returns: 

103 dict: A dictionary containing the JSON data. 

104 

105 """ 

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

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

108 

109 def _validate(self) -> bool: 

110 """Run the following validation tests: 

111 

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

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

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

115 

116 Returns: 

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

118 

119 """ 

120 # pylint: disable=multiple-statements 

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