python-simpleconf 0.6.0__tar.gz → 0.6.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.
@@ -1,30 +1,30 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: python-simpleconf
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Simple configuration management with python.
5
- Home-page: https://github.com/pwwang/simpleconf
6
5
  License: MIT
7
6
  Author: pwwang
8
7
  Author-email: pwwang@pwwang.com
9
- Requires-Python: >=3.7,<4.0
8
+ Requires-Python: >=3.9,<4.0
10
9
  Classifier: License :: OSI Approved :: MIT License
11
10
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.7
13
- Classifier: Programming Language :: Python :: 3.8
14
11
  Classifier: Programming Language :: Python :: 3.9
15
12
  Classifier: Programming Language :: Python :: 3.10
16
13
  Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
17
16
  Provides-Extra: all
18
17
  Provides-Extra: env
19
18
  Provides-Extra: ini
20
19
  Provides-Extra: toml
21
20
  Provides-Extra: yaml
22
- Requires-Dist: diot (>=0.2.1,<0.3.0)
21
+ Requires-Dist: diot (>=0.3.1,<0.4.0)
23
22
  Requires-Dist: iniconfig (>=2.0,<3.0) ; extra == "ini" or extra == "all"
24
- Requires-Dist: python-dotenv (>=0.21,<0.22) ; extra == "env" or extra == "all"
23
+ Requires-Dist: python-dotenv (>=1.0,<2.0) ; extra == "env" or extra == "all"
25
24
  Requires-Dist: pyyaml (>=6,<7) ; extra == "yaml" or extra == "all"
26
- Requires-Dist: rtoml (>=0.8,<0.9) ; (python_version < "3.11" and sys_platform == "linux") and (extra == "toml" or extra == "all")
27
- Requires-Dist: tomli (>=2.0,<3.0) ; (sys_platform != "linux" and python_version < "3.11") and (extra == "toml" or extra == "all")
25
+ Requires-Dist: rtoml (>=0.12,<0.13) ; (sys_platform == "linux") and (extra == "toml" or extra == "all")
26
+ Requires-Dist: tomli (>=2.0,<3.0) ; (sys_platform != "linux") and (extra == "toml" or extra == "all")
27
+ Project-URL: Homepage, https://github.com/pwwang/simpleconf
28
28
  Project-URL: Repository, https://github.com/pwwang/simpleconf
29
29
  Description-Content-Type: text/markdown
30
30
 
@@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "python-simpleconf"
7
- version = "0.6.0"
7
+ version = "0.6.2"
8
8
  description = "Simple configuration management with python."
9
9
  authors = [ "pwwang <pwwang@pwwang.com>",]
10
10
  license = "MIT"
@@ -16,13 +16,13 @@ packages = [
16
16
  ]
17
17
 
18
18
  [tool.poetry.dependencies]
19
- python = "^3.7"
20
- diot = "^0.2.1"
21
- python-dotenv = { version="^0.21", optional = true}
19
+ python = "^3.9"
20
+ diot = "^0.3.1"
21
+ python-dotenv = { version="^1.0", optional = true}
22
22
  pyyaml = { version="^6", optional = true}
23
23
  # Use rtoml only when the wheel is available (linux)
24
- rtoml = {version = "^0.8", optional = true, python = "<3.11", platform = "linux"}
25
- tomli = {version = "^2.0", optional = true, python = "<3.11", markers = 'sys_platform != "linux"'}
24
+ rtoml = {version = "^0.12", optional = true, platform = "linux"}
25
+ tomli = {version = "^2.0", optional = true, markers = 'sys_platform != "linux"'}
26
26
  iniconfig = {version = "^2.0", optional = true}
27
27
 
28
28
  [tool.poetry.extras]
@@ -33,8 +33,8 @@ toml = [ "rtoml", "tomli"]
33
33
  all = [ "iniconfig", "python-dotenv", "pyyaml", "rtoml", "tomli"]
34
34
 
35
35
  [tool.poetry.dev-dependencies]
36
- pytest = "^7"
37
- pytest-cov = "^4"
36
+ pytest = "^8"
37
+ pytest-cov = "^6"
38
38
 
39
39
  [tool.pytest.ini_options]
40
40
  addopts = "-vv -p no:asyncio --cov=simpleconf --cov-report xml:cov.xml --cov-report term-missing"
@@ -45,6 +45,6 @@ console_output_style = "progress"
45
45
  junit_family = "xunit1"
46
46
 
47
47
  [tool.black]
48
- line-length = 80
48
+ line-length = 88
49
49
  target-version = ['py37', 'py38', 'py39', 'py310']
50
50
  include = '\.pyi?$'
@@ -1,3 +1,3 @@
1
1
  from .config import Config, ProfileConfig
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.6.2"
@@ -1,8 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
- from typing import TYPE_CHECKING, Any, Callable, Sequence
4
+ from typing import Any, Callable, Sequence, TYPE_CHECKING
3
5
  from ast import literal_eval
4
6
 
5
- from simpleconf.utils import require_package
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from diot import Diot
@@ -47,6 +48,8 @@ def _cast_bool(value: str) -> bool:
47
48
 
48
49
  def _cast_toml(value: str) -> Any:
49
50
  """Cast toml string"""
51
+ from .utils import require_package
52
+
50
53
  toml = require_package("rtoml", "tomllib", "tomli")
51
54
  return toml.loads(value)
52
55
 
@@ -74,11 +77,11 @@ def cast_value(value: Any, casters: Sequence[Callable]) -> Any:
74
77
  return value
75
78
 
76
79
 
77
- def cast(conf: "Diot", casters: Sequence[Callable]) -> "Diot":
80
+ def cast(conf: Diot, casters: Sequence[Callable]) -> Diot:
78
81
  """Cast the configuration"""
79
82
  for key, value in conf.items():
80
83
  if isinstance(value, dict):
81
- conf[key] = cast(value, casters)
84
+ conf[key] = cast(value, casters) # type: ignore[arg-type]
82
85
  else:
83
86
  conf[key] = cast_value(value, casters)
84
87
  return conf
@@ -1,40 +1,60 @@
1
+ from __future__ import annotations
2
+
1
3
  from contextlib import contextmanager
2
- from typing import Any, List
4
+ from typing import Any, List, Generator, Union, Sequence
3
5
 
4
6
  from diot import Diot
5
7
 
6
8
  from .utils import config_to_ext, get_loader, POOL_KEY, META_KEY
9
+ from .loaders import Loader
10
+
11
+ LoaderType = Union[str, Loader, None]
7
12
 
8
13
 
9
14
  class Config:
10
15
  """The configuration class"""
11
16
 
12
17
  @staticmethod
13
- def load(*configs, ignore_nonexist: bool = False) -> Diot:
18
+ def load(
19
+ *configs: Any,
20
+ loader: LoaderType | Sequence[LoaderType] = None,
21
+ ignore_nonexist: bool = False,
22
+ ) -> Diot:
14
23
  """Load the configuration from the files, or other configurations
15
24
 
16
25
  Args:
17
26
  *configs: The configuration files or other configurations to load
18
27
  Latter ones will override the former ones for items with the
19
28
  same keys recursively.
29
+ loader: The loader to use. If a list is given, it must have the
30
+ same length as configs.
20
31
  ignore_nonexist: Whether to ignore non-existent files
21
32
  Otherwise, will raise errors
22
33
 
23
34
  Returns:
24
35
  A Diot object with the loaded configurations
25
36
  """
37
+ if not isinstance(loader, Sequence) or isinstance(loader, str):
38
+ loader = [loader] * len(configs)
39
+
40
+ if len(loader) != len(configs):
41
+ raise ValueError(
42
+ f"Length of loader ({len(loader)}) does not match "
43
+ f"length of configs ({len(configs)})"
44
+ )
45
+
26
46
  out = Diot()
27
- for conf in configs:
28
- ext = config_to_ext(conf)
29
- loader = get_loader(ext)
30
- loaded = loader.load(conf, ignore_nonexist)
47
+ for i, conf in enumerate(configs):
48
+ loaded = Config.load_one(conf, loader[i], ignore_nonexist)
31
49
  out.update_recursively(loaded)
32
50
 
33
51
  return out
34
52
 
35
53
  @staticmethod
36
54
  def load_one(
37
- config, loader: str = None, ignore_nonexist: bool = False
55
+ config,
56
+ loader: str | Loader | None = None,
57
+ ignore_nonexist: bool = False,
38
58
  ) -> Diot:
39
59
  """Load the configuration from the file
40
60
 
@@ -48,6 +68,9 @@ class Config:
48
68
  A Diot object with the loaded configuration
49
69
  """
50
70
  if loader is None:
71
+ if hasattr(config, "read"):
72
+ raise ValueError("'loader' must be specified for stream")
73
+
51
74
  ext = config_to_ext(config)
52
75
  loader = get_loader(ext)
53
76
  else:
@@ -60,26 +83,50 @@ class ProfileConfig:
60
83
  """The configuration class with profile support"""
61
84
 
62
85
  @staticmethod
63
- def load(*configs: Any, ignore_nonexist: bool = False) -> Diot:
86
+ def load(
87
+ *configs: Any,
88
+ loader: LoaderType | Sequence[LoaderType] = None,
89
+ ignore_nonexist: bool = False,
90
+ ) -> Diot:
64
91
  """Load the configuration from the files, or other configurations
65
92
 
66
93
  Args:
67
94
  *configs: The configuration files or other configurations to load
68
95
  Latter ones will override the former ones for items with the
69
96
  same profile and keys recursively.
97
+ loader: The loader to use. If a list is given, it must have the
98
+ same length as configs.
70
99
  ignore_nonexist: Whether to ignore non-existent files
71
100
  Otherwise, will raise errors
72
101
  """
102
+ if not isinstance(loader, Sequence) or isinstance(loader, str):
103
+ loader = [loader] * len(configs)
104
+
105
+ if len(loader) != len(configs):
106
+ raise ValueError(
107
+ f"Length of loader ({len(loader)}) does not match "
108
+ f"length of configs ({len(configs)})"
109
+ )
110
+
73
111
  out = Diot({POOL_KEY: Diot()})
74
112
  pool = out[POOL_KEY]
75
113
  out[META_KEY] = {
76
114
  "current_profile": None,
77
115
  "base_profile": None,
78
116
  }
79
- for conf in configs:
80
- ext = config_to_ext(conf)
81
- loader = get_loader(ext)
82
- loaded = loader.load_with_profiles(conf, ignore_nonexist)
117
+ for i, conf in enumerate(configs):
118
+ lder = loader[i]
119
+
120
+ if lder is None and hasattr(conf, "read"):
121
+ raise ValueError("'loader' must be specified for stream")
122
+
123
+ if lder is None:
124
+ ext = config_to_ext(conf)
125
+ lder = get_loader(ext)
126
+ else:
127
+ lder = get_loader(lder)
128
+
129
+ loaded = lder.load_with_profiles(conf, ignore_nonexist)
83
130
  for profile, value in loaded.items():
84
131
  profile = profile.lower()
85
132
  pool.setdefault(profile, Diot())
@@ -90,7 +137,9 @@ class ProfileConfig:
90
137
 
91
138
  @staticmethod
92
139
  def load_one(
93
- conf: Any, loader: str = None, ignore_nonexist: bool = False
140
+ conf: Any,
141
+ loader: str | Loader | None = None,
142
+ ignore_nonexist: bool = False,
94
143
  ) -> Diot:
95
144
  """Load the configuration from the file
96
145
 
@@ -112,6 +161,9 @@ class ProfileConfig:
112
161
  }
113
162
 
114
163
  if loader is None:
164
+ if hasattr(conf, "read"):
165
+ raise ValueError("'loader' must be specified for stream")
166
+
115
167
  ext = config_to_ext(conf)
116
168
  loader = get_loader(ext)
117
169
  else:
@@ -167,6 +219,8 @@ class ProfileConfig:
167
219
  conf[META_KEY]["current_profile"] = profile
168
220
  conf[META_KEY]["base_profile"] = base
169
221
 
222
+ return conf
223
+
170
224
  @staticmethod
171
225
  def current_profile(conf: Diot) -> str:
172
226
  """Get the current profile"""
@@ -228,7 +282,11 @@ class ProfileConfig:
228
282
 
229
283
  @staticmethod
230
284
  @contextmanager
231
- def with_profile(conf: Diot, profile: str, base: str = "default") -> Diot:
285
+ def with_profile(
286
+ conf: Diot,
287
+ profile: str,
288
+ base: str = "default",
289
+ ) -> Generator[Diot, None, None]:
232
290
  """A context manager to use the given profile
233
291
 
234
292
  Args:
@@ -1,4 +1,3 @@
1
-
2
1
  class FormatNotSupported(Exception):
3
2
  """Raised if format not supported"""
4
3
 
@@ -1,7 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  from abc import ABC, abstractmethod
2
4
  from os import PathLike
3
5
  from pathlib import Path
4
- from typing import TYPE_CHECKING, Any
6
+ from typing import TYPE_CHECKING, Any, Callable, List
5
7
 
6
8
  from ..caster import cast
7
9
 
@@ -11,7 +13,7 @@ if TYPE_CHECKING:
11
13
 
12
14
  class Loader(ABC):
13
15
 
14
- CASTERS = None
16
+ CASTERS: List[Callable[[str, bool], Any]] | None = None
15
17
 
16
18
  @abstractmethod
17
19
  def loading(self, conf: Any, ignore_nonexist: bool) -> "Diot":
@@ -1,4 +1,5 @@
1
1
  import warnings
2
+ import io
2
3
  from pathlib import Path
3
4
  from typing import Any
4
5
  from simpleconf.utils import require_package
@@ -36,11 +37,16 @@ class EnvLoader(Loader):
36
37
 
37
38
  def loading(self, conf: Any, ignore_nonexist: bool = False) -> Diot:
38
39
  """Load the configuration from a .env file"""
40
+ if hasattr(conf, "read"):
41
+ content = conf.read()
42
+ return Diot(dotenv.dotenv_values(stream=io.StringIO(content)))
43
+
39
44
  if not self._exists(conf, ignore_nonexist):
40
45
  return Diot()
46
+
41
47
  return Diot(dotenv.main.DotEnv(conf).dict())
42
48
 
43
- def load_with_profiles(
49
+ def load_with_profiles( # type: ignore[override]
44
50
  self,
45
51
  conf: Any,
46
52
  ignore_nonexist: bool = False,
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import warnings
2
4
  from typing import Any
3
5
  from pathlib import Path
@@ -34,37 +36,48 @@ class IniLoader(Loader):
34
36
  toml_caster,
35
37
  ]
36
38
 
37
- def loading(self, conf: Any, ignore_nonexist: bool) -> dict:
39
+ def loading(self, conf: Any, ignore_nonexist: bool) -> Diot:
38
40
  """Load the configuration from an ini-like file"""
41
+ if hasattr(conf, "read"):
42
+ content = conf.read()
43
+ return iniconfig.IniConfig("<config>", content).sections
44
+
39
45
  if not self._exists(conf, ignore_nonexist):
40
46
  return Diot(default={})
47
+
41
48
  return iniconfig.IniConfig(conf).sections
42
49
 
43
- def load(self, conf: Any, ignore_nonexist: bool = False) -> "Diot":
50
+ def load(self, conf: Any, ignore_nonexist: bool = False) -> Diot:
44
51
  """Load and cast the configuration from an ini-like file"""
45
52
  sections = self.loading(conf, ignore_nonexist)
46
53
  keys = list(sections)
54
+
55
+ if hasattr(conf, "read"):
56
+ pathname = "<config>"
57
+ else:
58
+ pathname = Path(conf).name
59
+
47
60
  # only load the default section
48
61
  if len(keys) > 1:
49
62
  warnings.warn(
50
- f"{Path(conf).name}: More than one section found, "
51
- "only the default section will be loaded."
63
+ f"{pathname}: More than one section found, "
64
+ "only the default section will be loaded. "
65
+ "Use ProfileConfig.load() if you want to sections as profiles."
52
66
  )
53
67
 
54
68
  if len(keys) == 0 or keys[0].lower() != "default":
55
69
  raise ValueError(
56
- f"{Path(conf).name}: Only the default section can be loaded."
70
+ f"{pathname}: Only the default section can be loaded."
57
71
  )
58
72
 
59
73
  return cast(Diot(sections[keys[0]]), self.__class__.CASTERS)
60
74
 
61
- def load_with_profiles(
75
+ def load_with_profiles( # type: ignore[override]
62
76
  self,
63
77
  conf: Any,
64
78
  ignore_nonexist: bool = False,
65
79
  ) -> Diot:
66
- """Load and cast the configuration from an ini-like file with profiles
67
- """
80
+ """Load and cast the configuration from an ini-like file with profiles"""
68
81
  sections = self.loading(conf, ignore_nonexist)
69
82
  out = Diot()
70
83
  for k, v in sections.items():
@@ -11,6 +11,10 @@ class JsonLoader(Loader):
11
11
 
12
12
  def loading(self, conf: Any, ignore_nonexist: bool) -> Diot:
13
13
  """Load the configuration from a json file"""
14
+ if hasattr(conf, "read"):
15
+ content = conf.read()
16
+ return Diot(json.loads(content))
17
+
14
18
  if not self._exists(conf, ignore_nonexist):
15
19
  return Diot()
16
20
  with open(conf) as f:
@@ -39,10 +39,10 @@ class OsenvLoader(Loader):
39
39
  out = Diot()
40
40
  for k, v in environ.items():
41
41
  if k.startswith(prefix):
42
- out[k[len_prefix :]] = v
42
+ out[k[len_prefix:]] = v
43
43
  return out
44
44
 
45
- def load_with_profiles(
45
+ def load_with_profiles( # type: ignore[override]
46
46
  self,
47
47
  conf: Any,
48
48
  ignore_nonexist: bool = False,
@@ -21,6 +21,10 @@ class TomlLoader(Loader):
21
21
 
22
22
  def loading(self, conf: Any, ignore_nonexist: bool) -> Diot:
23
23
  """Load the configuration from a toml file"""
24
+ if hasattr(conf, "read"):
25
+ content = conf.read()
26
+ return Diot(toml.loads(content))
27
+
24
28
  if not self._exists(conf, ignore_nonexist):
25
29
  return Diot()
26
30
 
@@ -28,5 +32,5 @@ class TomlLoader(Loader):
28
32
  with open(conf, "rb") as f:
29
33
  return Diot(toml.load(f))
30
34
 
31
- with open(conf, "r") as f:
35
+ with open(conf, "r") as f: # rtoml
32
36
  return Diot(toml.load(f))
@@ -12,6 +12,10 @@ class YamlLoader(Loader):
12
12
 
13
13
  def loading(self, conf: Any, ignore_nonexist: bool) -> Diot:
14
14
  """Load the configuration from a yaml file"""
15
+ if hasattr(conf, "read"):
16
+ content = conf.read()
17
+ return Diot(yaml.load(content, Loader=yaml.FullLoader))
18
+
15
19
  if not self._exists(conf, ignore_nonexist):
16
20
  return Diot()
17
21
 
File without changes
@@ -1,15 +1,12 @@
1
-
2
1
  from __future__ import annotations
3
2
 
4
3
  from pathlib import Path
5
4
  from importlib import import_module
6
5
  from types import ModuleType
7
- from typing import TYPE_CHECKING, Any
6
+ from typing import Any
8
7
 
9
8
  from .exceptions import FormatNotSupported
10
-
11
- if TYPE_CHECKING:
12
- from .loaders import Loader
9
+ from .loaders import Loader
13
10
 
14
11
  POOL_KEY = "_SIMPLECONF_POOL"
15
12
  META_KEY = "_SIMPLECONF_META"
@@ -34,28 +31,38 @@ def config_to_ext(conf: Any) -> str:
34
31
  return out
35
32
 
36
33
 
37
- def get_loader(ext: str) -> Loader:
34
+ def get_loader(ext: str | Loader) -> Loader:
38
35
  """Get the loader for the extension"""
36
+ if isinstance(ext, Loader):
37
+ return ext
38
+
39
39
  if ext == "dict":
40
40
  from .loaders.dict import DictLoader
41
+
41
42
  return DictLoader()
42
43
  if ext == "env":
43
44
  from .loaders.env import EnvLoader
45
+
44
46
  return EnvLoader()
45
47
  if ext == "ini":
46
48
  from .loaders.ini import IniLoader
49
+
47
50
  return IniLoader()
48
51
  if ext == "json":
49
52
  from .loaders.json import JsonLoader
53
+
50
54
  return JsonLoader()
51
55
  if ext == "osenv":
52
56
  from .loaders.osenv import OsenvLoader
57
+
53
58
  return OsenvLoader()
54
59
  if ext == "toml":
55
60
  from .loaders.toml import TomlLoader
61
+
56
62
  return TomlLoader()
57
63
  if ext == "yaml":
58
64
  from .loaders.yaml import YamlLoader
65
+
59
66
  return YamlLoader()
60
67
 
61
68
  raise FormatNotSupported(f"{ext} is not supported.")