Skip to content

Commit 48be5f8

Browse files
0.2.2
1 parent dd24b30 commit 48be5f8

File tree

15 files changed

+234
-38
lines changed

15 files changed

+234
-38
lines changed

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ jobs:
1212
- uses: actions/checkout@v2
1313
with:
1414
ref: master
15-
- name: Set up Python 3.8
15+
- name: Set up Python 3.10
1616
uses: actions/setup-python@v2
1717
with:
18-
python-version: 3.9
18+
python-version: '3.10'
1919

2020
- name: Install setuptools
2121
run: |

readme.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ It will create a mutable object from the model that holds the same values.
3232
Easyconfig also provides some mixin classes, so you can have type hints for the file load functions.
3333
These mixins are not required, they are just there to provide type hints in the IDE.
3434

35+
For convenience reasons you can also import ``AppBaseModel`` and ``BaseModel`` from ``easyconfig`` so you don't have to
36+
inherit from the mixins yourself.
37+
3538
### Simple example
3639

3740
```python
@@ -65,18 +68,20 @@ port: 443
6568
6669
### Nested example
6770
71+
Nested example with the convenience base classes from easyconfig.
72+
6873
```python
69-
from pydantic import BaseModel, Field
70-
from easyconfig import AppConfigMixin, ConfigMixin, create_app_config
74+
from pydantic import Field
75+
from easyconfig import AppBaseModel, BaseModel, create_app_config
7176

7277

73-
class HttpConfig(BaseModel, ConfigMixin):
78+
class HttpConfig(BaseModel):
7479
retries: int = 5
7580
url: str = 'localhost'
7681
port: int = 443
7782

7883

79-
class MyAppSimpleConfig(BaseModel, AppConfigMixin):
84+
class MyAppSimpleConfig(AppBaseModel):
8085
run_at: int = Field(12, alias='run at') # use alias to load from/create a different key
8186
http: HttpConfig = HttpConfig()
8287

@@ -94,16 +99,17 @@ http:
9499
port: 443
95100
```
96101
102+
97103
### Comments
98104
It's possible to specify a description through the pydantic ``Field``.
99105
The description will be created as a comment in the .yml file
100106
101107
```python
102-
from pydantic import BaseModel, Field
103-
from easyconfig import AppConfigMixin, create_app_config
108+
from pydantic import Field
109+
from easyconfig import AppBaseModel, create_app_config
104110

105111

106-
class MySimpleAppConfig(BaseModel, AppConfigMixin):
112+
class MySimpleAppConfig(AppBaseModel):
107113
retries: int = Field(5, description='Amount of retries on error')
108114
url: str = 'localhost'
109115
port: int = 443
@@ -125,11 +131,10 @@ when the configuration gets loaded for the first time. A useful feature if the a
125131
of the configuration file (e.g. through a file watcher).
126132
127133
```python
128-
from pydantic import BaseModel
129-
from easyconfig import AppConfigMixin, create_app_config
134+
from easyconfig import AppBaseModel, create_app_config
130135

131136

132-
class MySimpleAppConfig(BaseModel, AppConfigMixin):
137+
class MySimpleAppConfig(AppBaseModel):
133138
retries: int = 5
134139
url: str = 'localhost'
135140
port: int = 443
@@ -152,6 +157,12 @@ sub.cancel()
152157
```
153158

154159
# Changelog
160+
#### 0.2.2 (25.03.2022)
161+
- Added convenience base classes ``AppBaseModel`` and ``BaseModel``
162+
- Works with private attributes and class functions
163+
- Fixed an issue where multiline comments would not be created properly
164+
- Added an option to exclude entries from config file
165+
155166
#### 0.2.1 (25.03.2022)
156167
- Allow callbacks for file defaults
157168

requirements.txt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# runtime dependencies
2-
pydantic >= 1.9.0, < 2.0
3-
ruamel.yaml >= 0.17, < 0.18
4-
typing-extensions >= 4.1, < 5
2+
-r requirements_setup.txt
53

64
# testing dependencies
75
pytest >= 7.1, < 8

src/easyconfig/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
# isort: split
55

6-
from easyconfig.models import AppConfigMixin, ConfigMixin
6+
from easyconfig.models import AppBaseModel, AppBaseSettings, AppConfigMixin, BaseModel, BaseSettings, ConfigMixin
77

88
# isort: split
99

src/easyconfig/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.2.1'
1+
__version__ = '0.2.2'

src/easyconfig/config_objs/object_config.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
from inspect import getmembers, isfunction
12
from typing import Any, Callable, Dict, List, Tuple, Type, TYPE_CHECKING, TypeVar, Union
23

34
from pydantic import BaseModel
45
from pydantic.fields import ModelField
56
from typing_extensions import Final
67

8+
from easyconfig import AppConfigMixin
79
from easyconfig.__const__ import MISSING, MISSING_TYPE
810
from easyconfig.config_objs import ConfigObjSubscription, SubscriptionParent
911
from easyconfig.errors import DuplicateSubscriptionError, FunctionCallNotAllowedError
@@ -16,6 +18,9 @@
1618
HINT_CONFIG_OBJ_TYPE = Type[HINT_CONFIG_OBJ]
1719

1820

21+
NO_COPY = [n for n, o in getmembers(AppConfigMixin) if isfunction(o)]
22+
23+
1924
class ConfigObj:
2025
def __init__(self, model: BaseModel,
2126
path: Tuple[str, ...] = ('__root__', ),
@@ -24,8 +29,9 @@ def __init__(self, model: BaseModel,
2429
self._obj_parent: Final = parent
2530
self._obj_path: Final = path
2631

27-
self._obj_model_fields: Dict[str, ModelField] = model.__fields__
2832
self._obj_model_class: Final = model.__class__
33+
self._obj_model_fields: Dict[str, ModelField] = model.__fields__
34+
self._obj_model_private_attrs: List[str] = list(model.__private_attributes__.keys())
2935

3036
self._obj_keys: Tuple[str, ...] = tuple()
3137
self._obj_values: Dict[str, Any] = {}
@@ -40,8 +46,21 @@ def from_model(cls, model: BaseModel,
4046
path: Tuple[str, ...] = ('__root__', ),
4147
parent: Union[MISSING_TYPE, HINT_CONFIG_OBJ] = MISSING):
4248

43-
ret = cls(model, path, parent)
44-
49+
# Copy functions from the class definition to the child class
50+
functions = {}
51+
for name, member in getmembers(model.__class__):
52+
if not name.startswith('_') and name not in NO_COPY and isfunction(member):
53+
functions[name] = member
54+
55+
# Create a new class that pulls down the user defined functions if there are any
56+
# It's not possible to attach the functions to the existing class instance
57+
if functions:
58+
new_cls = type(f'{model.__class__.__name__}{cls.__name__}', (cls, ), functions)
59+
ret = new_cls(model, path, parent)
60+
else:
61+
ret = cls(model, path, parent)
62+
63+
# Set the values or create corresponding subclasses
4564
keys = []
4665
for key in ret._obj_model_fields.keys():
4766
value = getattr(model, key, MISSING)
@@ -62,13 +81,27 @@ def from_model(cls, model: BaseModel,
6281
# set child and values
6382
setattr(ret, key, attrib)
6483

84+
# copy private attributes - these are the same as values
85+
for key in ret._obj_model_private_attrs:
86+
value = getattr(model, key, MISSING)
87+
if value is MISSING:
88+
continue
89+
90+
keys.append(key)
91+
92+
ret._obj_values[key] = value
93+
setattr(ret, key, value)
94+
6595
ret._obj_keys = tuple(keys)
6696
return ret
6797

6898
def _set_values(self, obj: BaseModel) -> bool:
6999
if not isinstance(obj, BaseModel):
70100
raise ValueError(f'Instance of {BaseModel.__class__.__name__} expected, got {obj} ({type(obj)})!')
71101

102+
# Update last model so we can delegate function calls
103+
self._last_model = obj
104+
72105
value_changed = False
73106

74107
# Values of child objects
@@ -88,7 +121,7 @@ def _set_values(self, obj: BaseModel) -> bool:
88121
value = getattr(obj, key, MISSING)
89122
if value is MISSING:
90123
continue
91-
old_value = self._obj_values[key]
124+
old_value = self._obj_values.get(key, MISSING)
92125
self._obj_values[key] = value
93126

94127
# Update only values, child objects change in place
@@ -107,9 +140,9 @@ def _set_values(self, obj: BaseModel) -> bool:
107140
def __repr__(self):
108141
return f'<{self.__class__.__name__} {".".join(self._obj_path)}>'
109142

110-
def __getattr__(self, item):
111-
# delegate call to model
112-
return getattr(self._last_model, item)
143+
# def __getattr__(self, item):
144+
# # delegate call to model
145+
# return getattr(self._last_model, item)
113146

114147
# ------------------------------------------------------------------------------------------------------------------
115148
# Match class signature with the Mixin Classes

src/easyconfig/models/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
from .app import AppConfigMixin
22
from .config import ConfigMixin
3+
4+
# isort: split
5+
6+
# Convenience Classes with sensible defaults
7+
from .convenience import AppBaseModel, AppBaseSettings, BaseModel, BaseSettings
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pydantic
2+
3+
from easyconfig.models import AppConfigMixin, ConfigMixin
4+
5+
6+
class BaseModel(pydantic.BaseModel, ConfigMixin):
7+
8+
class Config:
9+
extra = pydantic.Extra.forbid
10+
11+
12+
class AppBaseModel(pydantic.BaseModel, AppConfigMixin):
13+
14+
class Config:
15+
extra = pydantic.Extra.forbid
16+
17+
18+
class BaseSettings(pydantic.BaseSettings, ConfigMixin):
19+
20+
class Config:
21+
extra = pydantic.Extra.forbid
22+
23+
24+
class AppBaseSettings(pydantic.BaseSettings, AppConfigMixin):
25+
26+
class Config:
27+
extra = pydantic.Extra.forbid

src/easyconfig/yaml/align.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,31 @@
66

77
def align_comments(d, extra_indent=0):
88

9-
# Only process when its a data structure -> dict or list
9+
# Only process when it's a data structure -> dict or list
1010
is_dict = isinstance(d, dict)
1111
if not is_dict and not isinstance(d, list):
1212
return None
1313

1414
comments = d.ca.items.values()
1515
if comments:
1616
max_col = max(map(lambda x: x[2].column, comments), default=0)
17+
indent_value = max_col + extra_indent
1718
for comment in comments:
18-
comment[2].column = max_col + extra_indent
19+
token = comment[2]
20+
token.column = indent_value
21+
22+
# workaround for multiline eol comments
23+
if '\n' in token.value:
24+
c_lines = token.value.splitlines() # type: list[str]
25+
for i, line in enumerate(c_lines):
26+
# first line is automatically indendet correctly
27+
if not i:
28+
continue
29+
30+
_line = line.lstrip()
31+
if _line:
32+
c_lines[i] = indent_value * ' ' + _line
33+
token.value = '\n'.join(c_lines)
1934

2035
for element in (d.values() if is_dict else d):
2136
align_comments(element, extra_indent=extra_indent)

src/easyconfig/yaml/from_model.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ def cmap_from_model(model: BaseModel, skip_none=True) -> CommentedMap:
1212
if value is MISSING or (skip_none and value is None):
1313
continue
1414

15+
field_info = field.field_info
16+
1517
yaml_key = field.alias
16-
description = field.field_info.description
18+
description = field_info.description
19+
20+
if not field_info.extra.get('in_file', True):
21+
continue
1722

1823
if isinstance(value, BaseModel):
1924
cmap[yaml_key] = cmap_from_model(value)
@@ -27,7 +32,15 @@ def cmap_from_model(model: BaseModel, skip_none=True) -> CommentedMap:
2732

2833
if not description:
2934
continue
35+
36+
# Don't overwrite comment
3037
if yaml_key not in cmap.ca.items:
31-
cmap.yaml_add_eol_comment(description, yaml_key)
38+
# Ensure that every line in the comment that has chars has a comment sign
39+
comment_lines = []
40+
for line in description.splitlines():
41+
_line = line.lstrip()
42+
comment_lines.append(('# ' + line) if _line and not _line.startswith('#') else line)
43+
44+
cmap.yaml_add_eol_comment('\n'.join(comment_lines), yaml_key)
3245

3346
return cmap

0 commit comments

Comments
 (0)