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
« 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.
7:Platform: Linux/Windows | Python 3.7+
8:Developer: J Berendt
9:Email: support@s3dev.uk
11:Comments: Basic concept `attribution`_.
14.. _attribution: https://stackoverflow.com/a/1639197/6340496
16"""
17# pylint: disable=too-few-public-methods
19import os
20import json
21from string import punctuation
24class Dict2Obj:
25 """Create a Python object from a standard Python dictionary, or JSON file.
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'.
32 - 'dict': a standard Python dictionary
33 - 'json': uses content from a JSON file
35 filepath (str, optional): Full file path to the JSON file to be used.
36 Defaults to None.
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.
43 :Note:
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.
52 :Example:
53 To convert a dictionary into an object::
55 >>> from utils4.dict2obj import Dict2Obj
57 >>> d = dict(a=1, b=2, title='This is a title.')
58 >>> obj = Dict2Obj(dictionary=d)
59 >>> print(obj.title)
61 This is a title.
63 """
65 _VALID = ['dict', 'json']
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()
74 def _create(self):
75 """Validate and create the object.
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..
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}.')
100 def _read_json(self) -> dict:
101 """Read values from a JSON file into a dictionary.
103 Returns:
104 dict: A dictionary containing the JSON data.
106 """
107 with open(self._fpath, 'r', encoding='utf-8') as f:
108 return json.loads(f.read())
110 def _validate(self) -> bool:
111 """Run the following validation tests:
113 - The ``source`` value is valid.
114 - If 'json' source, a file path is provided.
115 - If 'json' source, the provided file path exists.
117 Returns:
118 bool: True if **all** tests pass, otherwise False.
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
128 def _validate_fileexists(self) -> bool:
129 """Validation test: If a 'json' source, test the file path exists.
131 Raises:
132 ValueError: If the passed filepath is not a '.json' extension.
133 ValueError: If the passed filepath does not exist.
135 Returns:
136 bool: True if the source is 'dict'; or if source is 'json' and
137 the file exists, otherwise False.
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
153 def _validate_is_dict(self) -> bool:
154 """Validation test: Verify the object is a ``dict``.
156 Raises:
157 TypeError: If the passed object is not a ``dict``.
159 Returns:
160 bool: True if the passed object is a ``dict``.
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
168 def _validate_source(self) -> bool:
169 """Validation test: If a 'json' source, test a file path is provided.
171 Raises:
172 ValueError: If the source is 'json' and a filepath is not provided.
174 Returns:
175 bool: True if the source is 'dict'; or if source is 'json' and a
176 file path is provided.
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
183 def _validate_source_value(self) -> bool:
184 """Validation test: The value of the ``source`` parameter is valid.
186 Raises:
187 ValueError: If the source string is invalid.
189 Returns:
190 bool: True if a valid source.
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