Skip to content

Commit a420ab7

Browse files
authored
Merge pull request #43 from claui/user-config
Add support for user-configurable settings
2 parents e2ff699 + 2bb391b commit a420ab7

6 files changed

Lines changed: 106 additions & 5 deletions

File tree

{{ cookiecutter.pypi_package_name }}/doc/sphinx/conf.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def skip_module(app, what, name, obj, skip, options):
5050
{% if cookiecutter.include_executable == "y" -%}
5151
'{{ cookiecutter.python_package_name }}.__main__',
5252
'{{ cookiecutter.python_package_name }}.cli',
53+
{% endif -%}
54+
'{{ cookiecutter.python_package_name }}.config',
55+
{% if cookiecutter.include_executable == "y" -%}
5356
'{{ cookiecutter.python_package_name }}.fire_workarounds',
5457
{% endif -%}
5558
'{{ cookiecutter.python_package_name }}.version',
@@ -68,6 +71,9 @@ def setup(sphinx):
6871
{% if cookiecutter.include_executable == "y" -%}
6972
'**/{{ cookiecutter.python_package_name }}/__main__/**',
7073
'**/{{ cookiecutter.python_package_name }}/cli/**',
74+
{% endif -%}
75+
'**/{{ cookiecutter.python_package_name }}/config/**',
76+
{% if cookiecutter.include_executable == "y" -%}
7177
'**/{{ cookiecutter.python_package_name }}/fire_workarounds/**',
7278
{% endif -%}
7379
'**/{{ cookiecutter.python_package_name }}/version/**',

{{ cookiecutter.pypi_package_name }}/pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ name = "{{ cookiecutter.python_package_name }}"
2323
version = "{{ cookiecutter.project_version }}"
2424
description = "{{ cookiecutter.project_description }}"
2525
readme = ["README.md", "USAGE.md"]
26-
authors = [
27-
"{{ cookiecutter.author_full_name }} <{{ cookiecutter.author_email }}>",
28-
]
26+
authors = ["{{ cookiecutter.author_full_name }} <{{ cookiecutter.author_email }}>"]
2927
license = "{{ cookiecutter.project_license }}"
3028
# See https://pypi.org/pypi?%3Aaction=list_classifiers
3129
classifiers = [
@@ -43,6 +41,7 @@ python = ">={{ cookiecutter.python_version }}"
4341
colorama = "*"
4442
fire = "*"
4543
{%- endif %}
44+
pyxdg = "*"
4645

4746
[tool.poetry.group.dev.dependencies]
4847
autopep8 = "*"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""User configuration management."""
2+
3+
import json
4+
import os
5+
from typing import Any
6+
7+
import xdg.BaseDirectory # type: ignore
8+
9+
from .logging import get_logger
10+
from .settings import PACKAGE_NAME
11+
12+
logger = get_logger(__name__)
13+
14+
15+
def config_get(key: str, default: Any | None = None) -> Any | None:
16+
"""Reads a config from the first level of a JSON file by key.
17+
18+
:param key: the name of the top-level JSON property.
19+
20+
:param default:
21+
a default value if the property is not present in the JSON
22+
or if its value is `null`.
23+
Defaults to `None`.
24+
25+
:return:
26+
the value as a Python object.
27+
Returns `None` if the JSON value is `null` or if `key` does
28+
not exist as a property at the top-level.
29+
"""
30+
path = os.path.join(config_dir(), f'{PACKAGE_NAME}.json')
31+
if not os.path.exists(path):
32+
logger.debug(
33+
'File %s does not exist; interpreting %s == ', path, default
34+
)
35+
return default
36+
with open(path, 'r', encoding='utf-8') as stream:
37+
config_dict = json.load(stream)
38+
return config_dict.get(key, default)
39+
40+
41+
def config_set(key: str, value: Any) -> None:
42+
"""Writes a key-value pair to the config JSON file.
43+
44+
Overwrites an existing property by the given key.
45+
46+
:param key: the name of the top-level JSON property to store.
47+
48+
:param value: the value as a Python object, or `None` for `null`.
49+
"""
50+
path = os.path.join(config_dir(), f'{PACKAGE_NAME}.json')
51+
if os.path.exists(path):
52+
with open(path, 'r+', encoding='utf-8') as stream:
53+
config_dict = json.load(stream) or {}
54+
if value and value == config_dict.get(key, None):
55+
logger.debug('Configuration unchanged for key %s', key)
56+
return
57+
config_dict[key] = value
58+
logger.info('Writing new value for key %s: %s', key, value)
59+
stream.seek(0)
60+
stream.truncate(0)
61+
json.dump(config_dict, stream)
62+
os.fsync(stream)
63+
else:
64+
with open(path, 'x', encoding='utf-8') as stream:
65+
logger.info(
66+
'Creating configuration file %s with value %s: %s',
67+
path,
68+
key,
69+
value,
70+
)
71+
json.dump({key: value}, stream)
72+
os.fsync(stream)
73+
74+
logger.info('New value written')
75+
76+
77+
def config_dir(subdir: str | None = None) -> str:
78+
"""Returns the configuration directory for this package.
79+
80+
Respects the XDG settings. Also creates the directory if it does
81+
not exist.
82+
83+
:param subdir: an optional subdirectory, relative to the
84+
configuration directory.
85+
86+
:return: the absolute path to the directory.
87+
"""
88+
return str(
89+
xdg.BaseDirectory.save_config_path(
90+
os.path.join(PACKAGE_NAME, subdir)
91+
if subdir
92+
else PACKAGE_NAME
93+
)
94+
)

{{ cookiecutter.pypi_package_name }}/{{ cookiecutter.python_package_name }}/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
{% if cookiecutter.include_executable == "y" %}
33

44
class CliError(Exception):
5-
"""An user-facing error message."""
5+
"""A user-facing error message."""
66
{% endif -%}

{{ cookiecutter.pypi_package_name }}/{{ cookiecutter.python_package_name }}/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@
77
PACKAGE_ROOT = Path(__file__).parent.absolute()
88
PYPROJECT_TOML = PROJECT_ROOT / 'pyproject.toml'
99

10+
PACKAGE_NAME = '{{ cookiecutter.pypi_package_name }}'
11+
1012
debugMode = bool(os.getenv('{{ cookiecutter.python_package_name | upper }}_DEBUG'))

{{ cookiecutter.pypi_package_name }}/{{ cookiecutter.python_package_name }}/{{ cookiecutter.first_module_name }}.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
logger = get_logger(__name__)
1313

1414

15-
class {{ cookiecutter.first_module_name.capitalize() }}:
15+
class {{ cookiecutter.first_module_name.capitalize() }}: # pylint: disable=too-few-public-methods
1616
"""
1717
:param foobar:
1818
The Foobar to connect to.

0 commit comments

Comments
 (0)