quick-ini 3.1.4__tar.gz → 3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quick_ini-3.1.4 → quick_ini-3.2}/PKG-INFO +1 -1
- {quick_ini-3.1.4 → quick_ini-3.2}/quick_ini/quick_ini.py +79 -25
- {quick_ini-3.1.4 → quick_ini-3.2}/quick_ini.egg-info/PKG-INFO +1 -1
- {quick_ini-3.1.4 → quick_ini-3.2}/setup.py +1 -1
- quick_ini-3.2/tests/test_quickini.py +467 -0
- quick_ini-3.1.4/tests/test_quickini.py +0 -91
- {quick_ini-3.1.4 → quick_ini-3.2}/LICENSE +0 -0
- {quick_ini-3.1.4 → quick_ini-3.2}/README.md +0 -0
- {quick_ini-3.1.4 → quick_ini-3.2}/pyproject.toml +0 -0
- {quick_ini-3.1.4 → quick_ini-3.2}/quick_ini/__init__.py +0 -0
- {quick_ini-3.1.4 → quick_ini-3.2}/quick_ini.egg-info/SOURCES.txt +0 -0
- {quick_ini-3.1.4 → quick_ini-3.2}/quick_ini.egg-info/dependency_links.txt +0 -0
- {quick_ini-3.1.4 → quick_ini-3.2}/quick_ini.egg-info/top_level.txt +0 -0
- {quick_ini-3.1.4 → quick_ini-3.2}/setup.cfg +0 -0
- {quick_ini-3.1.4 → quick_ini-3.2}/tests/__init__.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from urllib.parse import urlparse
|
|
2
|
-
from typing import Type, Any, Union, Dict, Optional
|
|
2
|
+
from typing import Type, Any, Union, Dict, Optional, List
|
|
3
3
|
import os
|
|
4
4
|
|
|
5
5
|
class QuickIni:
|
|
6
|
-
parsed_ini: Dict[str, Union[str, int, bool, float, None]] = {}
|
|
6
|
+
parsed_ini: Dict[str, Union[str, int, bool, float, List, None]] = {}
|
|
7
7
|
loaded_file_path: Optional[str] = None
|
|
8
8
|
auto_type_convert_g: bool = True
|
|
9
9
|
error_message: str = ""
|
|
@@ -101,15 +101,45 @@ class QuickIni:
|
|
|
101
101
|
return False
|
|
102
102
|
|
|
103
103
|
lines = f_data.splitlines()
|
|
104
|
+
i = 0
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
while i < len(lines):
|
|
107
|
+
line = lines[i]
|
|
108
|
+
|
|
109
|
+
# Skip comments and lines without "="
|
|
110
|
+
if(line.startswith("#") or "=" not in line):
|
|
111
|
+
i += 1
|
|
112
|
+
continue
|
|
113
|
+
|
|
107
114
|
left, right = line.split("=", 1)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
|
|
116
|
+
# Check if this is an array (empty value and next line starts with |)
|
|
117
|
+
if not right.strip() and i + 1 < len(lines) and lines[i + 1].startswith("|"):
|
|
118
|
+
# This is an array, collect all array items
|
|
119
|
+
array_items = []
|
|
120
|
+
i += 1 # Move to the first array item
|
|
121
|
+
|
|
122
|
+
while i < len(lines) and lines[i].startswith("|"):
|
|
123
|
+
array_value = lines[i][1:] # Remove the | prefix
|
|
124
|
+
|
|
125
|
+
if auto_type_convert:
|
|
126
|
+
array_value = QuickIni.string_to_type(array_value)
|
|
127
|
+
|
|
128
|
+
array_items.append(array_value)
|
|
129
|
+
i += 1
|
|
130
|
+
|
|
131
|
+
QuickIni.parsed_ini[left] = array_items
|
|
132
|
+
else:
|
|
133
|
+
# Regular key-value pair
|
|
134
|
+
if(not right):
|
|
135
|
+
right = empty_default
|
|
136
|
+
elif(not auto_type_convert):
|
|
137
|
+
pass # If we aren't auto converting, skip the conversion
|
|
138
|
+
else:
|
|
139
|
+
right = QuickIni.string_to_type(right)
|
|
111
140
|
|
|
112
|
-
|
|
141
|
+
QuickIni.parsed_ini[left] = right
|
|
142
|
+
i += 1
|
|
113
143
|
|
|
114
144
|
QuickIni.loaded_file_path=file_location
|
|
115
145
|
return True
|
|
@@ -142,7 +172,7 @@ class QuickIni:
|
|
|
142
172
|
|
|
143
173
|
Parameters:
|
|
144
174
|
key (str): The key to update or add in the .ini file.
|
|
145
|
-
value (Any): The value to write, which will be converted to a string.
|
|
175
|
+
value (Any): The value to write, which will be converted to a string. If value is a list, it will be written as an array.
|
|
146
176
|
add_if_not_found (bool): If True, adds the key-value pair if the key is not found in the file (default is False).
|
|
147
177
|
update_localy (bool): If True, updates the parsed_ini dictionary locally with the new key-value pair (default is True).
|
|
148
178
|
do_backup (bool): If True, creates a backup of the original file before making changes, and removes if successful.
|
|
@@ -159,18 +189,25 @@ class QuickIni:
|
|
|
159
189
|
Example:
|
|
160
190
|
>>> write_value("example_key", 42, add_if_not_found=True)
|
|
161
191
|
42
|
|
192
|
+
>>> write_value("parts", ["one", "two", "three"], add_if_not_found=True)
|
|
193
|
+
["one", "two", "three"]
|
|
162
194
|
"""
|
|
163
195
|
|
|
164
196
|
if(not QuickIni.loaded_file_path):
|
|
165
197
|
QuickIni.error_message = "No loaded file path, was a local file loaded?"
|
|
166
198
|
raise(ValueError(QuickIni.error_message))
|
|
167
199
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
# Handle array values
|
|
201
|
+
if isinstance(value, list):
|
|
202
|
+
key_value_pair = f"{str(key)}=\n"
|
|
203
|
+
for item in value:
|
|
204
|
+
key_value_pair += f"|{str(item)}\n"
|
|
205
|
+
else:
|
|
206
|
+
try: str(value)
|
|
207
|
+
except(TypeError, ValueError):
|
|
208
|
+
QuickIni.error_message = "Could not convert value to a string"
|
|
209
|
+
raise(ValueError(QuickIni.error_message))
|
|
210
|
+
key_value_pair = f"{str(key)}={str(value)}\n"
|
|
174
211
|
|
|
175
212
|
try:
|
|
176
213
|
file = open(str(QuickIni.loaded_file_path), 'r', encoding='utf-8')
|
|
@@ -185,25 +222,37 @@ class QuickIni:
|
|
|
185
222
|
print("Backup created at", f"{str(QuickIni.loaded_file_path)}.backup")
|
|
186
223
|
|
|
187
224
|
key_found=False
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
225
|
+
new_lines = []
|
|
226
|
+
i = 0
|
|
227
|
+
|
|
228
|
+
while i < len(lines):
|
|
229
|
+
line = lines[i]
|
|
230
|
+
|
|
191
231
|
if(not line.startswith("#") and not key_found and line.startswith(f"{key}=")):
|
|
192
|
-
|
|
232
|
+
# Found the key, replace it and skip any existing array items
|
|
233
|
+
new_lines.append(key_value_pair)
|
|
193
234
|
key_found = True
|
|
235
|
+
i += 1
|
|
236
|
+
|
|
237
|
+
# Skip existing array items if they exist
|
|
238
|
+
while i < len(lines) and lines[i].startswith("|"):
|
|
239
|
+
i += 1
|
|
194
240
|
else:
|
|
195
|
-
|
|
196
|
-
|
|
241
|
+
new_lines.append(line)
|
|
242
|
+
i += 1
|
|
197
243
|
|
|
198
244
|
if(not key_found):
|
|
199
245
|
if(add_if_not_found):
|
|
200
|
-
if
|
|
201
|
-
|
|
202
|
-
|
|
246
|
+
if new_lines and not new_lines[-1].endswith("\n"):
|
|
247
|
+
new_lines.append("\n")
|
|
248
|
+
new_lines.append(key_value_pair)
|
|
203
249
|
else:
|
|
204
250
|
QuickIni.error_message = f"Could not find key '{key}'"
|
|
205
251
|
raise(ValueError(QuickIni.error_message))
|
|
206
252
|
|
|
253
|
+
# Write the modified content
|
|
254
|
+
file = open(str(QuickIni.loaded_file_path), 'w')
|
|
255
|
+
file.writelines(new_lines)
|
|
207
256
|
file.close()
|
|
208
257
|
|
|
209
258
|
except FileNotFoundError as e:
|
|
@@ -216,7 +265,12 @@ class QuickIni:
|
|
|
216
265
|
QuickIni.error_message = f"IOError when reading or writting to the file '{QuickIni.loaded_file_path}'"
|
|
217
266
|
raise(e)
|
|
218
267
|
|
|
219
|
-
|
|
268
|
+
# Handle return value
|
|
269
|
+
if isinstance(value, list):
|
|
270
|
+
v = [QuickIni.string_to_type(str(item)) if QuickIni.auto_type_convert_g else str(item) for item in value]
|
|
271
|
+
else:
|
|
272
|
+
v = QuickIni.string_to_type(str(value)) if QuickIni.auto_type_convert_g else str(value)
|
|
273
|
+
|
|
220
274
|
if(update_locally):
|
|
221
275
|
QuickIni.parsed_ini[str(key)] = v
|
|
222
276
|
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name="quick_ini",
|
|
8
|
-
version="3.
|
|
8
|
+
version="3.2",
|
|
9
9
|
description="A library for reading from and writing to .ini formatted files",
|
|
10
10
|
long_description=long_description,
|
|
11
11
|
long_description_content_type="text/markdown",
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#
|
|
2
|
+
# WRITTEN ENTIRELY BY CHATGPT
|
|
3
|
+
# I DON'T TRUST IT
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
import tempfile
|
|
8
|
+
import os
|
|
9
|
+
from unittest.mock import patch, mock_open
|
|
10
|
+
from quick_ini import QuickIni
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestQuickIni(unittest.TestCase):
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
"""Set up test fixtures before each test method."""
|
|
17
|
+
# Clear any previous state
|
|
18
|
+
QuickIni.parsed_ini = {}
|
|
19
|
+
QuickIni.loaded_file_path = None
|
|
20
|
+
QuickIni.auto_type_convert_g = True
|
|
21
|
+
QuickIni.error_message = ""
|
|
22
|
+
QuickIni.web_file = None
|
|
23
|
+
|
|
24
|
+
# Create a temporary INI file for testing
|
|
25
|
+
self.test_content = """# Test configuration
|
|
26
|
+
debug=true
|
|
27
|
+
port=8080
|
|
28
|
+
timeout=30.5
|
|
29
|
+
name=TestApp
|
|
30
|
+
empty_value=
|
|
31
|
+
false_value=false
|
|
32
|
+
|
|
33
|
+
# Array example
|
|
34
|
+
parts=
|
|
35
|
+
|one
|
|
36
|
+
|two
|
|
37
|
+
|three
|
|
38
|
+
|
|
39
|
+
# Mixed array
|
|
40
|
+
mixed=
|
|
41
|
+
|text
|
|
42
|
+
|42
|
|
43
|
+
|3.14
|
|
44
|
+
|true
|
|
45
|
+
|false
|
|
46
|
+
|
|
47
|
+
# Comments and edge cases
|
|
48
|
+
# This is a comment
|
|
49
|
+
another_key=value with spaces
|
|
50
|
+
"""
|
|
51
|
+
self.temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.ini', delete=False)
|
|
52
|
+
self.temp_file.write(self.test_content)
|
|
53
|
+
self.temp_file.close()
|
|
54
|
+
|
|
55
|
+
def tearDown(self):
|
|
56
|
+
"""Clean up after each test method."""
|
|
57
|
+
if os.path.exists(self.temp_file.name):
|
|
58
|
+
os.unlink(self.temp_file.name)
|
|
59
|
+
|
|
60
|
+
# ===== BASIC LOADING TESTS =====
|
|
61
|
+
|
|
62
|
+
def test_load_file_success(self):
|
|
63
|
+
"""Test loading an INI file successfully."""
|
|
64
|
+
result = QuickIni.load_file(self.temp_file.name)
|
|
65
|
+
self.assertTrue(result)
|
|
66
|
+
self.assertEqual(QuickIni.loaded_file_path, self.temp_file.name)
|
|
67
|
+
|
|
68
|
+
def test_load_file_nonexistent(self):
|
|
69
|
+
"""Test loading a nonexistent file."""
|
|
70
|
+
result = QuickIni.load_file("nonexistent_file.ini")
|
|
71
|
+
self.assertFalse(result)
|
|
72
|
+
error = QuickIni.get_last_error()
|
|
73
|
+
self.assertIn("Could not find file", error)
|
|
74
|
+
|
|
75
|
+
def test_load_file_with_auto_type_convert_disabled(self):
|
|
76
|
+
"""Test loading with auto type conversion disabled."""
|
|
77
|
+
result = QuickIni.load_file(self.temp_file.name, auto_type_convert=False)
|
|
78
|
+
self.assertTrue(result)
|
|
79
|
+
# All values should be strings when auto_type_convert is False
|
|
80
|
+
self.assertEqual(QuickIni.get_value("port"), "8080")
|
|
81
|
+
self.assertEqual(QuickIni.get_value("debug"), "true")
|
|
82
|
+
self.assertEqual(QuickIni.get_value("timeout"), "30.5")
|
|
83
|
+
|
|
84
|
+
def test_load_file_with_empty_default(self):
|
|
85
|
+
"""Test loading with custom empty default value."""
|
|
86
|
+
result = QuickIni.load_file(self.temp_file.name, empty_default="EMPTY")
|
|
87
|
+
self.assertTrue(result)
|
|
88
|
+
self.assertEqual(QuickIni.get_value("empty_value"), "EMPTY")
|
|
89
|
+
|
|
90
|
+
# ===== TYPE CONVERSION TESTS =====
|
|
91
|
+
|
|
92
|
+
def test_string_to_type_integer(self):
|
|
93
|
+
"""Test string to integer conversion."""
|
|
94
|
+
self.assertEqual(QuickIni.string_to_type("123"), 123)
|
|
95
|
+
self.assertEqual(QuickIni.string_to_type("0"), 0)
|
|
96
|
+
|
|
97
|
+
def test_string_to_type_float(self):
|
|
98
|
+
"""Test string to float conversion."""
|
|
99
|
+
self.assertEqual(QuickIni.string_to_type("123.45"), 123.45)
|
|
100
|
+
self.assertEqual(QuickIni.string_to_type("0.0"), 0.0)
|
|
101
|
+
|
|
102
|
+
def test_string_to_type_boolean(self):
|
|
103
|
+
"""Test string to boolean conversion."""
|
|
104
|
+
self.assertEqual(QuickIni.string_to_type("true"), True)
|
|
105
|
+
self.assertEqual(QuickIni.string_to_type("True"), True)
|
|
106
|
+
self.assertEqual(QuickIni.string_to_type("TRUE"), True)
|
|
107
|
+
self.assertEqual(QuickIni.string_to_type("false"), False)
|
|
108
|
+
self.assertEqual(QuickIni.string_to_type("False"), False)
|
|
109
|
+
self.assertEqual(QuickIni.string_to_type("FALSE"), False)
|
|
110
|
+
|
|
111
|
+
def test_string_to_type_string(self):
|
|
112
|
+
"""Test string remains string when not convertible."""
|
|
113
|
+
self.assertEqual(QuickIni.string_to_type("hello"), "hello")
|
|
114
|
+
self.assertEqual(QuickIni.string_to_type("123abc"), "123abc")
|
|
115
|
+
self.assertEqual(QuickIni.string_to_type("12.34.56"), "12.34.56")
|
|
116
|
+
|
|
117
|
+
# ===== GET VALUE TESTS =====
|
|
118
|
+
|
|
119
|
+
def test_get_value_string(self):
|
|
120
|
+
"""Test getting string values."""
|
|
121
|
+
QuickIni.load_file(self.temp_file.name)
|
|
122
|
+
value = QuickIni.get_value("name")
|
|
123
|
+
self.assertEqual(value, "TestApp")
|
|
124
|
+
self.assertIsInstance(value, str)
|
|
125
|
+
|
|
126
|
+
def test_get_value_boolean_true(self):
|
|
127
|
+
"""Test getting boolean true values."""
|
|
128
|
+
QuickIni.load_file(self.temp_file.name)
|
|
129
|
+
value = QuickIni.get_value("debug")
|
|
130
|
+
self.assertTrue(value)
|
|
131
|
+
self.assertIsInstance(value, bool)
|
|
132
|
+
|
|
133
|
+
def test_get_value_boolean_false(self):
|
|
134
|
+
"""Test getting boolean false values."""
|
|
135
|
+
QuickIni.load_file(self.temp_file.name)
|
|
136
|
+
value = QuickIni.get_value("false_value")
|
|
137
|
+
self.assertFalse(value)
|
|
138
|
+
self.assertIsInstance(value, bool)
|
|
139
|
+
|
|
140
|
+
def test_get_value_integer(self):
|
|
141
|
+
"""Test getting integer values."""
|
|
142
|
+
QuickIni.load_file(self.temp_file.name)
|
|
143
|
+
value = QuickIni.get_value("port")
|
|
144
|
+
self.assertEqual(value, 8080)
|
|
145
|
+
self.assertIsInstance(value, int)
|
|
146
|
+
|
|
147
|
+
def test_get_value_float(self):
|
|
148
|
+
"""Test getting float values."""
|
|
149
|
+
QuickIni.load_file(self.temp_file.name)
|
|
150
|
+
value = QuickIni.get_value("timeout")
|
|
151
|
+
self.assertEqual(value, 30.5)
|
|
152
|
+
self.assertIsInstance(value, float)
|
|
153
|
+
|
|
154
|
+
def test_get_value_default(self):
|
|
155
|
+
"""Test getting default values for missing keys."""
|
|
156
|
+
QuickIni.load_file(self.temp_file.name)
|
|
157
|
+
value = QuickIni.get_value("missing_key", "default")
|
|
158
|
+
self.assertEqual(value, "default")
|
|
159
|
+
|
|
160
|
+
def test_get_value_with_type_checking(self):
|
|
161
|
+
"""Test get_value with expected_type parameter."""
|
|
162
|
+
QuickIni.load_file(self.temp_file.name)
|
|
163
|
+
|
|
164
|
+
# Valid type checks should pass
|
|
165
|
+
value = QuickIni.get_value("port", expected_type=int)
|
|
166
|
+
self.assertEqual(value, 8080)
|
|
167
|
+
|
|
168
|
+
value = QuickIni.get_value("name", expected_type=str)
|
|
169
|
+
self.assertEqual(value, "TestApp")
|
|
170
|
+
|
|
171
|
+
# Invalid type checks should raise ValueError
|
|
172
|
+
with self.assertRaises(ValueError):
|
|
173
|
+
QuickIni.get_value("port", expected_type=str)
|
|
174
|
+
|
|
175
|
+
def test_get_value_spaces_in_value(self):
|
|
176
|
+
"""Test getting values with spaces."""
|
|
177
|
+
QuickIni.load_file(self.temp_file.name)
|
|
178
|
+
value = QuickIni.get_value("another_key")
|
|
179
|
+
self.assertEqual(value, "value with spaces")
|
|
180
|
+
|
|
181
|
+
# ===== ARRAY TESTS =====
|
|
182
|
+
|
|
183
|
+
def test_get_array_value(self):
|
|
184
|
+
"""Test getting array values."""
|
|
185
|
+
QuickIni.load_file(self.temp_file.name)
|
|
186
|
+
parts = QuickIni.get_value("parts")
|
|
187
|
+
self.assertEqual(parts, ["one", "two", "three"])
|
|
188
|
+
self.assertIsInstance(parts, list)
|
|
189
|
+
|
|
190
|
+
def test_get_mixed_array_with_type_conversion(self):
|
|
191
|
+
"""Test getting mixed type arrays with automatic conversion."""
|
|
192
|
+
QuickIni.load_file(self.temp_file.name)
|
|
193
|
+
mixed = QuickIni.get_value("mixed")
|
|
194
|
+
self.assertEqual(mixed[0], "text")
|
|
195
|
+
self.assertEqual(mixed[1], 42)
|
|
196
|
+
self.assertEqual(mixed[2], 3.14)
|
|
197
|
+
self.assertEqual(mixed[3], True)
|
|
198
|
+
self.assertEqual(mixed[4], False)
|
|
199
|
+
|
|
200
|
+
def test_array_without_type_conversion(self):
|
|
201
|
+
"""Test arrays without automatic type conversion."""
|
|
202
|
+
QuickIni.load_file(self.temp_file.name, auto_type_convert=False)
|
|
203
|
+
mixed = QuickIni.get_value("mixed")
|
|
204
|
+
# All should be strings
|
|
205
|
+
self.assertEqual(mixed, ["text", "42", "3.14", "true", "false"])
|
|
206
|
+
|
|
207
|
+
def test_write_array_value(self):
|
|
208
|
+
"""Test writing array values to file."""
|
|
209
|
+
QuickIni.load_file(self.temp_file.name)
|
|
210
|
+
test_array = ["apple", "banana", "cherry"]
|
|
211
|
+
result = QuickIni.write_value("fruits", test_array, add_if_not_found=True)
|
|
212
|
+
self.assertEqual(result, test_array)
|
|
213
|
+
|
|
214
|
+
# Verify it was written as an array
|
|
215
|
+
fruits = QuickIni.get_value("fruits")
|
|
216
|
+
self.assertEqual(fruits, test_array)
|
|
217
|
+
self.assertIsInstance(fruits, list)
|
|
218
|
+
|
|
219
|
+
def test_write_mixed_array(self):
|
|
220
|
+
"""Test writing mixed type arrays."""
|
|
221
|
+
QuickIni.load_file(self.temp_file.name)
|
|
222
|
+
mixed_array = ["text", 42, 3.14, True, False]
|
|
223
|
+
QuickIni.write_value("new_mixed", mixed_array, add_if_not_found=True)
|
|
224
|
+
|
|
225
|
+
# Reload to test parsing
|
|
226
|
+
QuickIni.load_file(self.temp_file.name)
|
|
227
|
+
result = QuickIni.get_value("new_mixed")
|
|
228
|
+
self.assertEqual(result, mixed_array)
|
|
229
|
+
|
|
230
|
+
def test_replace_existing_array(self):
|
|
231
|
+
"""Test replacing an existing array."""
|
|
232
|
+
QuickIni.load_file(self.temp_file.name)
|
|
233
|
+
original_parts = QuickIni.get_value("parts")
|
|
234
|
+
self.assertEqual(original_parts, ["one", "two", "three"])
|
|
235
|
+
|
|
236
|
+
# Replace with new array
|
|
237
|
+
new_parts = ["alpha", "beta", "gamma"]
|
|
238
|
+
QuickIni.write_value("parts", new_parts)
|
|
239
|
+
|
|
240
|
+
# Verify replacement
|
|
241
|
+
updated_parts = QuickIni.get_value("parts")
|
|
242
|
+
self.assertEqual(updated_parts, new_parts)
|
|
243
|
+
|
|
244
|
+
# ===== WRITE VALUE TESTS =====
|
|
245
|
+
|
|
246
|
+
def test_write_value_new_key(self):
|
|
247
|
+
"""Test writing new key-value pairs."""
|
|
248
|
+
QuickIni.load_file(self.temp_file.name)
|
|
249
|
+
result = QuickIni.write_value("new_key", "new_value", add_if_not_found=True)
|
|
250
|
+
self.assertEqual(result, "new_value")
|
|
251
|
+
|
|
252
|
+
# Verify it was written
|
|
253
|
+
value = QuickIni.get_value("new_key")
|
|
254
|
+
self.assertEqual(value, "new_value")
|
|
255
|
+
|
|
256
|
+
def test_write_value_existing_key(self):
|
|
257
|
+
"""Test updating existing key-value pairs."""
|
|
258
|
+
QuickIni.load_file(self.temp_file.name)
|
|
259
|
+
# Update existing value
|
|
260
|
+
result = QuickIni.write_value("name", "UpdatedApp")
|
|
261
|
+
self.assertEqual(result, "UpdatedApp")
|
|
262
|
+
|
|
263
|
+
# Verify it was updated
|
|
264
|
+
value = QuickIni.get_value("name")
|
|
265
|
+
self.assertEqual(value, "UpdatedApp")
|
|
266
|
+
|
|
267
|
+
def test_write_value_key_not_found_without_add_flag(self):
|
|
268
|
+
"""Test writing to non-existent key without add_if_not_found flag."""
|
|
269
|
+
QuickIni.load_file(self.temp_file.name)
|
|
270
|
+
with self.assertRaises(ValueError) as context:
|
|
271
|
+
QuickIni.write_value("nonexistent_key", "value")
|
|
272
|
+
self.assertIn("Could not find key", str(context.exception))
|
|
273
|
+
|
|
274
|
+
def test_write_value_no_file_loaded(self):
|
|
275
|
+
"""Test writing when no file is loaded."""
|
|
276
|
+
with self.assertRaises(ValueError) as context:
|
|
277
|
+
QuickIni.write_value("key", "value")
|
|
278
|
+
self.assertIn("No loaded file path", str(context.exception))
|
|
279
|
+
|
|
280
|
+
def test_write_value_with_backup(self):
|
|
281
|
+
"""Test writing with backup functionality."""
|
|
282
|
+
QuickIni.load_file(self.temp_file.name)
|
|
283
|
+
backup_path = f"{self.temp_file.name}.backup"
|
|
284
|
+
|
|
285
|
+
# Ensure backup doesn't exist initially
|
|
286
|
+
if os.path.exists(backup_path):
|
|
287
|
+
os.remove(backup_path)
|
|
288
|
+
|
|
289
|
+
QuickIni.write_value("name", "BackupTest", do_backup=True)
|
|
290
|
+
|
|
291
|
+
# Backup should be created and then removed
|
|
292
|
+
self.assertFalse(os.path.exists(backup_path))
|
|
293
|
+
|
|
294
|
+
def test_write_value_update_locally_false(self):
|
|
295
|
+
"""Test writing without updating local dictionary."""
|
|
296
|
+
QuickIni.load_file(self.temp_file.name)
|
|
297
|
+
original_value = QuickIni.get_value("name")
|
|
298
|
+
|
|
299
|
+
QuickIni.write_value("name", "NotUpdatedLocally", update_locally=False)
|
|
300
|
+
|
|
301
|
+
# Local value should remain unchanged
|
|
302
|
+
current_value = QuickIni.get_value("name")
|
|
303
|
+
self.assertEqual(current_value, original_value)
|
|
304
|
+
|
|
305
|
+
# ===== URL FUNCTIONALITY TESTS =====
|
|
306
|
+
|
|
307
|
+
def test_is_string_url_valid(self):
|
|
308
|
+
"""Test URL validation with valid URLs."""
|
|
309
|
+
self.assertTrue(QuickIni.is_string_url("http://example.com"))
|
|
310
|
+
self.assertTrue(QuickIni.is_string_url("https://example.com"))
|
|
311
|
+
self.assertTrue(QuickIni.is_string_url("ftp://example.com"))
|
|
312
|
+
|
|
313
|
+
def test_is_string_url_invalid(self):
|
|
314
|
+
"""Test URL validation with invalid URLs."""
|
|
315
|
+
self.assertFalse(QuickIni.is_string_url("not_a_url"))
|
|
316
|
+
self.assertFalse(QuickIni.is_string_url("file.txt"))
|
|
317
|
+
self.assertFalse(QuickIni.is_string_url(""))
|
|
318
|
+
|
|
319
|
+
@patch('urllib.request.urlopen')
|
|
320
|
+
def test_grab_from_url_success_with_different_status(self, mock_urlopen):
|
|
321
|
+
"""Test successful URL loading with status 200."""
|
|
322
|
+
# Mock successful HTTP response with explicit status check
|
|
323
|
+
mock_response = mock_urlopen.return_value.__enter__.return_value
|
|
324
|
+
mock_response.status = 200
|
|
325
|
+
mock_response.read.return_value = b"status_test=success"
|
|
326
|
+
|
|
327
|
+
result = QuickIni.grab_from_url("http://example.com/status.ini")
|
|
328
|
+
self.assertTrue(result)
|
|
329
|
+
self.assertEqual(QuickIni.web_file, "status_test=success")
|
|
330
|
+
|
|
331
|
+
@patch('urllib.request.urlopen')
|
|
332
|
+
def test_grab_from_url_non_200_status(self, mock_urlopen):
|
|
333
|
+
"""Test URL loading with non-200 status code."""
|
|
334
|
+
# Mock non-200 HTTP response
|
|
335
|
+
mock_response = mock_urlopen.return_value.__enter__.return_value
|
|
336
|
+
mock_response.status = 500
|
|
337
|
+
|
|
338
|
+
result = QuickIni.grab_from_url("http://example.com/error.ini")
|
|
339
|
+
self.assertFalse(result)
|
|
340
|
+
self.assertIn("Failed to download file", QuickIni.get_last_error())
|
|
341
|
+
self.assertIn("got response: 500", QuickIni.get_last_error())
|
|
342
|
+
|
|
343
|
+
@patch('urllib.request.urlopen')
|
|
344
|
+
def test_grab_from_url_http_error_exception(self, mock_urlopen):
|
|
345
|
+
"""Test URL loading with HTTPError exception."""
|
|
346
|
+
from urllib.error import HTTPError
|
|
347
|
+
# Mock HTTPError exception
|
|
348
|
+
mock_urlopen.side_effect = HTTPError(None, 404, "Not Found", None, None)
|
|
349
|
+
|
|
350
|
+
result = QuickIni.grab_from_url("http://example.com/notfound.ini")
|
|
351
|
+
self.assertFalse(result)
|
|
352
|
+
self.assertIn("Could not get file from URL", QuickIni.get_last_error())
|
|
353
|
+
|
|
354
|
+
def test_is_string_url_value_error(self):
|
|
355
|
+
"""Test URL validation with malformed URL that raises ValueError."""
|
|
356
|
+
# Test with a string that causes urlparse to raise ValueError
|
|
357
|
+
result = QuickIni.is_string_url("ht tp://invalid url")
|
|
358
|
+
self.assertFalse(result)
|
|
359
|
+
|
|
360
|
+
def test_write_value_permission_error(self):
|
|
361
|
+
"""Test write_value with permission errors."""
|
|
362
|
+
QuickIni.load_file(self.temp_file.name)
|
|
363
|
+
|
|
364
|
+
# Mock file operations to raise PermissionError
|
|
365
|
+
with patch('builtins.open', side_effect=PermissionError("Permission denied")):
|
|
366
|
+
with self.assertRaises(PermissionError):
|
|
367
|
+
QuickIni.write_value("test_key", "test_value")
|
|
368
|
+
|
|
369
|
+
def test_write_value_io_error(self):
|
|
370
|
+
"""Test write_value with I/O errors."""
|
|
371
|
+
QuickIni.load_file(self.temp_file.name)
|
|
372
|
+
|
|
373
|
+
# Mock file operations to raise IOError
|
|
374
|
+
with patch('builtins.open', side_effect=IOError("I/O error")):
|
|
375
|
+
with self.assertRaises(IOError):
|
|
376
|
+
QuickIni.write_value("test_key", "test_value")
|
|
377
|
+
|
|
378
|
+
def test_write_value_backup_removal_failure(self):
|
|
379
|
+
"""Test backup removal failure handling."""
|
|
380
|
+
QuickIni.load_file(self.temp_file.name)
|
|
381
|
+
|
|
382
|
+
# Mock os.remove to raise an exception
|
|
383
|
+
with patch('os.remove', side_effect=OSError("Cannot remove backup")):
|
|
384
|
+
with self.assertRaises(OSError):
|
|
385
|
+
QuickIni.write_value("test_key", "test_value", add_if_not_found=True, do_backup=True)
|
|
386
|
+
|
|
387
|
+
@patch('urllib.request.urlopen')
|
|
388
|
+
def test_load_file_from_url(self, mock_urlopen):
|
|
389
|
+
"""Test loading INI file from URL."""
|
|
390
|
+
# Mock successful HTTP response
|
|
391
|
+
mock_response = mock_urlopen.return_value.__enter__.return_value
|
|
392
|
+
mock_response.status = 200
|
|
393
|
+
mock_response.read.return_value = b"url_key=url_value\nurl_port=9090"
|
|
394
|
+
|
|
395
|
+
result = QuickIni.load_file("http://example.com/config.ini")
|
|
396
|
+
self.assertTrue(result)
|
|
397
|
+
self.assertEqual(QuickIni.get_value("url_key"), "url_value")
|
|
398
|
+
self.assertEqual(QuickIni.get_value("url_port"), 9090)
|
|
399
|
+
|
|
400
|
+
# ===== ERROR HANDLING TESTS =====
|
|
401
|
+
|
|
402
|
+
def test_get_last_error(self):
|
|
403
|
+
"""Test error message retrieval."""
|
|
404
|
+
# Trigger an error
|
|
405
|
+
QuickIni.load_file("nonexistent_file.ini")
|
|
406
|
+
error = QuickIni.get_last_error()
|
|
407
|
+
self.assertIsInstance(error, str)
|
|
408
|
+
self.assertTrue(len(error) > 0)
|
|
409
|
+
|
|
410
|
+
def test_file_io_error_handling(self):
|
|
411
|
+
"""Test handling of file I/O errors."""
|
|
412
|
+
# Test with invalid file path
|
|
413
|
+
result = QuickIni.load_file("/invalid/path/file.ini")
|
|
414
|
+
self.assertFalse(result)
|
|
415
|
+
error = QuickIni.get_last_error()
|
|
416
|
+
self.assertIn("Could not find file", error)
|
|
417
|
+
|
|
418
|
+
# ===== EDGE CASES =====
|
|
419
|
+
|
|
420
|
+
def test_empty_ini_file(self):
|
|
421
|
+
"""Test loading empty INI file."""
|
|
422
|
+
empty_file = tempfile.NamedTemporaryFile(mode='w', suffix='.ini', delete=False)
|
|
423
|
+
empty_file.write("")
|
|
424
|
+
empty_file.close()
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
result = QuickIni.load_file(empty_file.name)
|
|
428
|
+
self.assertTrue(result)
|
|
429
|
+
# Should have no parsed values
|
|
430
|
+
self.assertEqual(len(QuickIni.parsed_ini), 0)
|
|
431
|
+
finally:
|
|
432
|
+
os.unlink(empty_file.name)
|
|
433
|
+
|
|
434
|
+
def test_comments_only_file(self):
|
|
435
|
+
"""Test loading file with only comments."""
|
|
436
|
+
comments_file = tempfile.NamedTemporaryFile(mode='w', suffix='.ini', delete=False)
|
|
437
|
+
comments_file.write("# Comment 1\n# Comment 2\n# Comment 3")
|
|
438
|
+
comments_file.close()
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
result = QuickIni.load_file(comments_file.name)
|
|
442
|
+
self.assertTrue(result)
|
|
443
|
+
# Should have no parsed values
|
|
444
|
+
self.assertEqual(len(QuickIni.parsed_ini), 0)
|
|
445
|
+
finally:
|
|
446
|
+
os.unlink(comments_file.name)
|
|
447
|
+
|
|
448
|
+
def test_malformed_lines(self):
|
|
449
|
+
"""Test handling of malformed lines without equals signs."""
|
|
450
|
+
malformed_file = tempfile.NamedTemporaryFile(mode='w', suffix='.ini', delete=False)
|
|
451
|
+
malformed_file.write("valid_key=valid_value\nmalformed_line_no_equals\nanother_valid=value")
|
|
452
|
+
malformed_file.close()
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
result = QuickIni.load_file(malformed_file.name)
|
|
456
|
+
self.assertTrue(result)
|
|
457
|
+
# Should only have valid lines
|
|
458
|
+
self.assertEqual(QuickIni.get_value("valid_key"), "valid_value")
|
|
459
|
+
self.assertEqual(QuickIni.get_value("another_valid"), "value")
|
|
460
|
+
# Malformed line should be ignored
|
|
461
|
+
self.assertIsNone(QuickIni.get_value("malformed_line_no_equals"))
|
|
462
|
+
finally:
|
|
463
|
+
os.unlink(malformed_file.name)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
if __name__ == '__main__':
|
|
467
|
+
unittest.main()
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
import tempfile
|
|
3
|
-
import os
|
|
4
|
-
from quick_ini import QuickIni
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestQuickIni(unittest.TestCase):
|
|
8
|
-
|
|
9
|
-
def setUp(self):
|
|
10
|
-
"""Set up test fixtures before each test method."""
|
|
11
|
-
# Create a temporary INI file for testing
|
|
12
|
-
self.test_content = """# Test configuration
|
|
13
|
-
debug=true
|
|
14
|
-
port=8080
|
|
15
|
-
timeout=30.5
|
|
16
|
-
name=TestApp
|
|
17
|
-
empty_value=
|
|
18
|
-
|
|
19
|
-
# Array example
|
|
20
|
-
parts=
|
|
21
|
-
|one
|
|
22
|
-
|two
|
|
23
|
-
|three
|
|
24
|
-
"""
|
|
25
|
-
self.temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.ini', delete=False)
|
|
26
|
-
self.temp_file.write(self.test_content)
|
|
27
|
-
self.temp_file.close()
|
|
28
|
-
|
|
29
|
-
def tearDown(self):
|
|
30
|
-
"""Clean up after each test method."""
|
|
31
|
-
if os.path.exists(self.temp_file.name):
|
|
32
|
-
os.unlink(self.temp_file.name)
|
|
33
|
-
|
|
34
|
-
def test_load_file(self):
|
|
35
|
-
"""Test loading an INI file."""
|
|
36
|
-
result = QuickIni.load_file(self.temp_file.name)
|
|
37
|
-
self.assertTrue(result)
|
|
38
|
-
|
|
39
|
-
def test_get_value_string(self):
|
|
40
|
-
"""Test getting string values."""
|
|
41
|
-
QuickIni.load_file(self.temp_file.name)
|
|
42
|
-
value = QuickIni.get_value("name")
|
|
43
|
-
self.assertEqual(value, "TestApp")
|
|
44
|
-
|
|
45
|
-
def test_get_value_boolean(self):
|
|
46
|
-
"""Test getting boolean values."""
|
|
47
|
-
QuickIni.load_file(self.temp_file.name)
|
|
48
|
-
value = QuickIni.get_value("debug")
|
|
49
|
-
self.assertTrue(value)
|
|
50
|
-
self.assertIsInstance(value, bool)
|
|
51
|
-
|
|
52
|
-
def test_get_value_integer(self):
|
|
53
|
-
"""Test getting integer values."""
|
|
54
|
-
QuickIni.load_file(self.temp_file.name)
|
|
55
|
-
value = QuickIni.get_value("port")
|
|
56
|
-
self.assertEqual(value, 8080)
|
|
57
|
-
self.assertIsInstance(value, int)
|
|
58
|
-
|
|
59
|
-
def test_get_value_float(self):
|
|
60
|
-
"""Test getting float values."""
|
|
61
|
-
QuickIni.load_file(self.temp_file.name)
|
|
62
|
-
value = QuickIni.get_value("timeout")
|
|
63
|
-
self.assertEqual(value, 30.5)
|
|
64
|
-
self.assertIsInstance(value, float)
|
|
65
|
-
|
|
66
|
-
def test_get_value_default(self):
|
|
67
|
-
"""Test getting default values for missing keys."""
|
|
68
|
-
QuickIni.load_file(self.temp_file.name)
|
|
69
|
-
value = QuickIni.get_value("missing_key", "default")
|
|
70
|
-
self.assertEqual(value, "default")
|
|
71
|
-
|
|
72
|
-
def test_write_value(self):
|
|
73
|
-
"""Test writing values to file."""
|
|
74
|
-
QuickIni.load_file(self.temp_file.name)
|
|
75
|
-
result = QuickIni.write_value("new_key", "new_value", add_if_not_found=True)
|
|
76
|
-
self.assertEqual(result, "new_value")
|
|
77
|
-
|
|
78
|
-
# Verify it was written
|
|
79
|
-
value = QuickIni.get_value("new_key")
|
|
80
|
-
self.assertEqual(value, "new_value")
|
|
81
|
-
|
|
82
|
-
def test_error_handling(self):
|
|
83
|
-
"""Test error handling for missing files."""
|
|
84
|
-
result = QuickIni.load_file("nonexistent_file.ini")
|
|
85
|
-
self.assertFalse(result)
|
|
86
|
-
error = QuickIni.get_last_error()
|
|
87
|
-
self.assertIn("Could not find file", error)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if __name__ == '__main__':
|
|
91
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|