Skip to content

Commit 0cfa136

Browse files
authored
option to sort alphabetically + when_read_only_try_to_add_write_permission (#66)
* option to sort alphabetically * add test
1 parent d227adc commit 0cfa136

File tree

2 files changed

+86
-2
lines changed

2 files changed

+86
-2
lines changed

pgserviceparser/__init__.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import configparser
33
import io
44
import platform
5+
import stat
56
from os import getenv
67
from pathlib import Path
78
from typing import Optional
@@ -10,6 +11,51 @@
1011
from .exceptions import ServiceFileNotFound, ServiceNotFound
1112

1213

14+
def _make_file_writable(path: Path):
15+
"""Attempt to add write permissions to a file.
16+
17+
Args:
18+
path: path to the file
19+
20+
Raises:
21+
PermissionError: when the file permissions cannot be changed
22+
"""
23+
current_permission = stat.S_IMODE(path.stat().st_mode)
24+
WRITE = stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH
25+
path.chmod(current_permission | WRITE)
26+
27+
28+
def _when_read_only_try_to_add_write_permission(func):
29+
"""Decorator for functions that attempt to modify the service file.
30+
31+
If the file is read-only, a PermissionError exception will be raised.
32+
This decorator handles that error by attempting to set write permissions
33+
(which works if the user is the owner of the file or has proper rights to
34+
alter the file permissions), and rerunning the decorated function.
35+
36+
If the user cannot modify permissions on the file, the PermissionError
37+
is re-raised.
38+
"""
39+
40+
def wrapper(*args, **kwargs):
41+
attempt = 0
42+
while attempt <= 1:
43+
try:
44+
return func(*args, **kwargs)
45+
except PermissionError:
46+
if attempt == 1:
47+
raise
48+
49+
try:
50+
_make_file_writable(conf_path())
51+
except PermissionError:
52+
pass
53+
finally:
54+
attempt += 1
55+
56+
return wrapper
57+
58+
1359
def conf_path(create_if_missing: Optional[bool] = False) -> Path:
1460
"""Returns the path found for the pg_service.conf on the system as string.
1561
@@ -66,6 +112,7 @@ def full_config(conf_file_path: Optional[Path] = None) -> configparser.ConfigPar
66112
return config
67113

68114

115+
@_when_read_only_try_to_add_write_permission
69116
def remove_service(service_name: str, conf_file_path: Optional[Path] = None) -> None:
70117
"""Remove a complete service from the service file.
71118
@@ -92,6 +139,7 @@ def remove_service(service_name: str, conf_file_path: Optional[Path] = None) ->
92139
config.write(configfile, space_around_delimiters=False)
93140

94141

142+
@_when_read_only_try_to_add_write_permission
95143
def rename_service(old_name: str, new_name: str, conf_file_path: Optional[Path] = None) -> None:
96144
"""Rename a service in the service file.
97145
@@ -124,6 +172,7 @@ def rename_service(old_name: str, new_name: str, conf_file_path: Optional[Path]
124172
config.write(configfile, space_around_delimiters=False)
125173

126174

175+
@_when_read_only_try_to_add_write_permission
127176
def create_service(service_name: str, settings: dict, conf_file_path: Optional[Path] = None) -> bool:
128177
"""Create a new service in the service file.
129178
@@ -153,6 +202,7 @@ def create_service(service_name: str, settings: dict, conf_file_path: Optional[P
153202
return True
154203

155204

205+
@_when_read_only_try_to_add_write_permission
156206
def copy_service_settings(
157207
source_service_name: str,
158208
target_service_name: str,
@@ -217,6 +267,7 @@ def service_config(service_name: str, conf_file_path: Optional[Path] = None) ->
217267
return dict(config[service_name])
218268

219269

270+
@_when_read_only_try_to_add_write_permission
220271
def write_service_setting(
221272
service_name: str,
222273
setting_key: str,
@@ -250,6 +301,7 @@ def write_service_setting(
250301
config.write(configfile, space_around_delimiters=False)
251302

252303

304+
@_when_read_only_try_to_add_write_permission
253305
def write_service(
254306
service_name: str, settings: dict, conf_file_path: Optional[Path] = None, create_if_not_found: bool = False
255307
) -> dict:
@@ -309,11 +361,13 @@ def write_service_to_text(service_name: str, settings: dict) -> str:
309361
return res.strip()
310362

311363

312-
def service_names(conf_file_path: Optional[Path] = None) -> list[str]:
364+
def service_names(conf_file_path: Optional[Path] = None, sorted_alphabetically: bool = False) -> list[str]:
313365
"""Returns all service names in a list.
314366
315367
Args:
316368
conf_file_path: path to the pg_service.conf. If None the `conf_path()` is used, defaults to None
369+
sorted_alphabetically: whether to sort the names alphabetically (case-insensitive),
370+
defaults to False
317371
318372
Returns:
319373
list of every service registered
@@ -323,4 +377,5 @@ def service_names(conf_file_path: Optional[Path] = None) -> list[str]:
323377
"""
324378

325379
config = full_config(conf_file_path)
326-
return config.sections()
380+
names = config.sections()
381+
return sorted(names, key=str.lower) if sorted_alphabetically else names

test/test_lib.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import os
1818
import shutil
19+
import stat
1920
import unittest
2021
from pathlib import Path
2122

@@ -57,6 +58,22 @@ def test_full_config(self):
5758
def test_service_names(self):
5859
self.assertEqual(service_names(), ["service_1", "service_2", "service_3", "service_4"])
5960

61+
def test_service_names_sorted_alphabetically(self):
62+
# Add a service whose name comes before existing ones alphabetically
63+
create_service("Alpha_service", {"host": "localhost"})
64+
create_service("zulu_service", {"host": "localhost"})
65+
66+
# Without sorting, order is as written in the file (appended at the end)
67+
names = service_names()
68+
self.assertEqual(names[-2:], ["Alpha_service", "zulu_service"])
69+
70+
# With sorting, names are case-insensitive alphabetical
71+
sorted_names = service_names(sorted_alphabetically=True)
72+
self.assertEqual(
73+
sorted_names,
74+
["Alpha_service", "service_1", "service_2", "service_3", "service_4", "zulu_service"],
75+
)
76+
6077
def test_service_config(self):
6178
self.assertRaises(ServiceNotFound, service_config, "non_existing_service")
6279

@@ -228,6 +245,18 @@ def test_remove_service(self):
228245
self.assertIn("service_tmp", service_names())
229246
remove_service("service_tmp")
230247

248+
def test_write_on_read_only_file(self):
249+
# Make the service file read-only
250+
self.service_file_path.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
251+
252+
# The decorator should automatically add write permission and succeed
253+
write_service_setting("service_1", "port", "9999")
254+
conf = service_config("service_1")
255+
self.assertEqual(conf["port"], "9999")
256+
257+
# Verify the file is writable again
258+
self.assertTrue(os.access(self.service_file_path, os.W_OK))
259+
231260
def test_missing_file(self):
232261
another_service_file_path = PGSERVICEPARSER_SRC_PATH / "test" / "data" / "new_folder" / "pgservice.conf"
233262
os.environ["PGSERVICEFILE"] = str(another_service_file_path)

0 commit comments

Comments
 (0)