Skip to content

Commit 16691f5

Browse files
committed
Further changes toward 2.0.0 release
1 parent 72c7497 commit 16691f5

35 files changed

+8259
-4122
lines changed

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,51 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [2.0.0] - Unreleased
8+
9+
### Added
10+
11+
- `firebird.base.buffer.MemoryBuffer.get_raw` method.
12+
- `get_raw` method to `BufferFactory`, `BytesBufferFactory` and `CTypesBufferFactory`.
13+
14+
### Changed
15+
16+
- Tests changed from `unittest` to `pytest`, 96% code coverage.
17+
- Minimal Python version raised to 3.11.
18+
- The `firebird.base.logging` module was completelly reworked.
19+
- Function `firebird.base.types.Conjunctive` renamed to `conjunctive`.
20+
- `firebird.base.collections.DataList.__init__` parameter `frozen` is now keyword-only.
21+
- `firebird.base.collections.DataList.extract` parameter `copy` is now keyword-only.
22+
- `firebird.base.collections.DataList.sort` parameter `reverse` is now keyword-only.
23+
- `firebird.base.collections.DataList.split` parameter `frozen` is now keyword-only.
24+
- `firebird.base.collections.Registry.popitem` parameter `last` is now keyword-only.
25+
- `firebird.base.collections.BaseObjectCollection.contains` parameter `expr` now does not have default value.
26+
- Deprecated `firebird.base.config.create_config` function was removed.
27+
- `firebird.base.config.DirectoryScheme` parameter `force_home` is now keyword only.
28+
- `firebird.base.config.Option` parameters `required` and `default` are now keyword only.
29+
- Parameter `context` was removed from `firebird.base.trace.traced` decorator.
30+
- Option `context` was removed from `firebird.base.trace.BaseTraceConfig`.
31+
- Log function return value as `repr` rather than `str`.
32+
33+
### Fixed
34+
35+
- Broken `firebird.base.types.Distinct` support for dataclasses and hash function.
36+
- Raise `BufferError` istead `IOError` in `firebird.base.buffer.MemoryBuffer` methods `resize`,
37+
`read` and `read_number`
38+
- Problem with `firebird.base.collections.Registry.pop` that did not raised `KeyError` when
39+
`default` was not specified.
40+
- Bug in `firebird.base.collections.Registry.popitem` with `last` = True.
41+
- Problem with name handling in `firebird.base.config.ConfigOption.clear` and `set_value`.
42+
- Problem with `firebird.base.config.WindowsDirectoryScheme` and `firebird.base.config.MacOSDirectoryScheme` constructors.
43+
- Problem with `firebird.base.config.ListOption.item_types` value.
44+
- Problem with internal `.Convertor` initialization in `firebird.base.config.ListOption`.
45+
- Use copy of `default` list stead direct use in `firebird.base.config.ListOption`.
46+
- `firebird.base.config.ListOption.get_formatted` and `firebird.base.config.ListOption.get_as_str`
47+
should return typed values for multitype lists.
48+
- `firebird.base.config.ConfigOption.validate` should validate the `Config` as well if defined.
49+
- `firebird.base.config.ConfigListOption.validate` should report error for empty list when `required`.
50+
- Problem with conversion of flags from string in `firebird.base.strconv`.
51+
752
## [1.8.0] - 2024-05-03
853

954
### Added

README.md

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,14 @@ supports multiple usage strategies.
111111
### Context-based logging
112112

113113
Module `logging` provides context-based logging system built on top of standard `logging`
114-
module.
114+
module. It also solves the common logging management problem when various modules use hard-coded
115+
separate loggers, and provides several types of message wrappers that allow lazy message
116+
interpolation using f-string, brace (`str.format`) or dollar (`string.Template`) formats.
115117

116118
The context-based logging:
117119

118-
- Adds context information (defined as combination of topic, agent and context string values)
119-
into `logging.LogRecord`, that could be used in logging message.
120-
- Adds support for f-string message format.
121-
- Allows assignment of loggers to specific contexts. The `LoggingManager` class maintains
122-
a set of bindings between `Logger` objects and combination of `agent`, `context` and `topic`
123-
specifications. It’s possible to bind loggers to exact combination of values, or whole
124-
sets of values using `ANY` sentinel. It means that is possible to assign specific Logger
125-
to log messages for particular agent in any context, or any agent operating in specific
126-
context etc.
120+
1. Adds context information into `logging.LogRecord`, that could be used in logging entry formats.
121+
2. Allows assignment of loggers to specific contexts.
127122

128123
### Trace/audit for class instances
129124

docs/changelog.txt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ Changelog
55
Version 2.0.0 (unreleased)
66
==========================
77

8-
* Change tests from `unittest` to `pytest`, almost complete code coverage.
8+
* Change tests from `unittest` to `pytest`, 96% code coverage.
99
* Minimal Python version raised to 3.11.
1010
* Code cleanup and optimization for Python 3.11 features.
1111
* `~firebird.base.types` module:
1212

1313
- Change: Function `Conjunctive` renamed to `.conjunctive`.
14+
- Fix: `.Distinct` support for dataclasses was broken.
15+
- Fix: `.Distinct` support for `hash` was broken.
1416

1517
* `~firebird.base.buffer` module:
1618

@@ -25,13 +27,25 @@ Version 2.0.0 (unreleased)
2527
- `.DataList.sort` parameter `reverse` is now keyword-only.
2628
- `.DataList.split` parameter `frozen` is now keyword-only.
2729
- `.Registry.popitem` parameter `last` is now keyword-only.
30+
- `.BaseObjectCollection.contains` parameter `expr` now does not have default value.
31+
- Fix: problem with `.Registry.pop` that did not raised `KeyError` when `default` was
32+
not specified.
33+
- Fix: bug in `.Registry.popitem` with `last` = True.
2834

2935
* `~firebird.base.config` module:
3036

3137
- Deprecated `.create_config` function was removed.
32-
- Change: `DirectoryScheme` parameter `force_home` is now keyword only.
33-
- Change: `Option` parameters `required` and `default` are now keyword only.
38+
- Change: `.DirectoryScheme` parameter `force_home` is now keyword only.
39+
- Change: `.Option` parameters `required` and `default` are now keyword only.
3440
- Fix: Problem with name handling in `.ConfigOption.clear` and `set_value`.
41+
- Fix: Problem with `.WindowsDirectoryScheme` and `.MacOSDirectoryScheme` constructors.
42+
- Fix: Problem with `.ListOption.item_types` value.
43+
- Fix: Problem with internal `.Convertor` initialization in `.ListOption`.
44+
- Fix: Use copy of `default` list stead direct use in `.ListOption`.
45+
- Fix: `.ListOption.get_formatted` and `.ListOption.get_as_str` should return typed values
46+
for multitype lists.
47+
- Fix: `.ConfigOption.validate` should validate the `.Config` as well if defined.
48+
- Fix: `.ConfigListOption.validate` should report error for empty list when `required`.
3549

3650
* `~firebird.base.strconv` module:
3751

src/firebird/base/collections.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def find(self, expr: FilterExpr, default: Any=None) -> Item:
140140
for item in self.filter(expr):
141141
return item
142142
return default
143-
def contains(self, expr: FilterExpr=None) -> bool:
143+
def contains(self, expr: FilterExpr) -> bool:
144144
"""Returns True if there is any item for which `expr` is evaluated as True.
145145
146146
Arguments:
@@ -593,18 +593,24 @@ def copy(self) -> Registry:
593593
self._reg = data
594594
c.update(self)
595595
return c
596-
def pop(self, key: Any, default: Any=None) -> Distinct:
596+
def pop(self, key: Any, default: Any=...) -> Distinct:
597597
"""Remove specified `key` and return the corresponding `.Distinct` object. If `key`
598598
is not found, the `default` is returned if given, otherwise `KeyError` is raised.
599599
"""
600-
return self._reg.pop(key.get_key() if isinstance(key, Distinct) else key, default)
600+
if default is ...:
601+
return self._reg.pop(key.get_key() if isinstance(key, Distinct) else key)
602+
else:
603+
return self._reg.pop(key.get_key() if isinstance(key, Distinct) else key, default)
601604
def popitem(self, *, last: bool=True) -> Distinct:
602605
"""Returns and removes a `.Distinct` object. The objects are returned in LIFO order
603606
if `last` is true or FIFO order if false.
604607
"""
605608
if last:
606609
_, item = self._reg.popitem()
607610
return item
608-
item = next(iter(self))
609-
self.remove(item)
610-
return item
611+
try:
612+
item = next(iter(self))
613+
self.remove(item)
614+
return item
615+
except StopIteration:
616+
raise KeyError()

src/firebird/base/config.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ def __init__(self, name: str, version: str | None=None, *, force_home: bool=Fals
389389
force_home: When True, general directories (i.e. all except user-specific and
390390
TMP) would be always based on HOME directory.
391391
"""
392-
super().__init__(name, version, force_home)
392+
super().__init__(name, version, force_home=force_home)
393393
app_dir = Path(self.name)
394394
if self.version is not None:
395395
app_dir /= self.version
@@ -462,7 +462,7 @@ def __init__(self, name: str, version: str | None=None, *, force_home: bool=Fals
462462
name: Appplication name.
463463
version: Application version.
464464
"""
465-
super().__init__(name, version, force_home)
465+
super().__init__(name, version, force_home=force_home)
466466
app_dir = Path(self.name)
467467
if self.version is not None:
468468
app_dir /= self.version
@@ -733,8 +733,9 @@ def get_config(self, *, plain: bool=False) -> str:
733733
"""
734734
if self.optional and not self.name:
735735
return ''
736-
lines = [f"[{self.name}]\n", ';\n']
736+
lines = [f"[{self.name}]\n"]
737737
if not plain:
738+
lines.append(';\n')
738739
for line in self.get_description().strip().splitlines():
739740
lines.append(f"; {line}\n")
740741
for option in self.options:
@@ -1146,7 +1147,7 @@ def set_as_str(self, value: str) -> None:
11461147
try:
11471148
self._value = Decimal(value)
11481149
except DecimalException as exc:
1149-
raise ValueError(str(exc)) from exc
1150+
raise ValueError("Cannot convert string to Decimal") from exc
11501151
def get_as_str(self) -> str:
11511152
"""Returns value as string.
11521153
"""
@@ -1747,14 +1748,17 @@ def __init__(self, name: str, item_type: type | Sequence[type], description: str
17471748
self._value: list = None
17481749
#: Datatypes of list items. If there is more than one type, each value in
17491750
#: config file must have format: `type_name:value_as_str`.
1750-
self.item_types: Sequence[type] = (item_type, ) if isinstance(item_type, type) else item_type
1751+
self.item_types: Sequence[type] = item_type if isinstance(item_type, Sequence) else (item_type, )
17511752
#: String that separates list item values when options value is read from
17521753
#: `ConfigParser`. Default separator is None. It's possible to use a line break as
17531754
#: separator. If separator is `None` and the value contains line breaks, it uses
17541755
#: the line break as separator, otherwise it uses comma as separator.
17551756
self.separator: str | None = separator
1756-
self._convertor: Convertor = get_convertor(item_type) if isinstance(item_type, type) else None
1757+
self._convertor: Convertor = get_convertor(item_type) if not isinstance(item_type, Sequence) else None
17571758
super().__init__(name, list, description, required=required, default=default)
1759+
# Value fixup, store copy of default list instead direct assignment
1760+
if default is not None:
1761+
self.set_value(list(default))
17581762
def _get_value_description(self) -> str:
17591763
return f"list [{', '.join(x.__name__ for x in self.item_types)}]\n"
17601764
def _check_value(self, value: list) -> None:
@@ -1776,13 +1780,13 @@ def clear(self, *, to_default: bool=True) -> None:
17761780
Arguments:
17771781
to_default: If True, sets the option value to default value, else to None.
17781782
"""
1779-
self._value = self.default if to_default else None
1783+
self._value = list(self.default) if to_default and self.default is not None else None
17801784
def get_formatted(self) -> str:
17811785
"""Returns value formatted for use in config file.
17821786
"""
17831787
if self._value is None:
17841788
return '<UNDEFINED>'
1785-
result = [convert_to_str(i) for i in self._value]
1789+
result = [self._get_as_typed_str(i) for i in self._value]
17861790
sep = self.separator
17871791
if sep is None:
17881792
sep = '\n' if sum(len(i) for i in result) > 80 else ',' # noqa: PLR2004
@@ -1821,7 +1825,7 @@ def set_as_str(self, value: str) -> None:
18211825
def get_as_str(self) -> str:
18221826
"""Returns value as string.
18231827
"""
1824-
result = [convert_to_str(i) for i in self._value]
1828+
result = [self._get_as_typed_str(i) for i in self._value]
18251829
sep = self.separator
18261830
if sep is None:
18271831
sep = '\n' if sum(len(i) for i in result) > 80 else ',' # noqa: PLR2004
@@ -2227,6 +2231,8 @@ def validate(self) -> None:
22272231
"""
22282232
if self.required and self.get_value().name == '':
22292233
raise Error(f"Missing value for required option '{self.name}'")
2234+
if self.get_value().name != '':
2235+
self.value.validate()
22302236
def clear(self, *, to_default: bool=True) -> None:
22312237
"""Clears the option value.
22322238
@@ -2368,6 +2374,14 @@ def clear(self, *, to_default: bool=True) -> None: # noqa: ARG002
23682374
to_default: As ConfigListOption does not have default value, this parameter is ignored.
23692375
"""
23702376
self._value.clear()
2377+
def validate(self) -> None:
2378+
"""Validates option state.
2379+
2380+
Raises:
2381+
Error: When required option does not have a value.
2382+
"""
2383+
if self.required and len(self.get_value()) == 0:
2384+
raise Error(f"Missing value for required option '{self.name}'")
23712385
def get_formatted(self) -> str:
23722386
"""Returns value formatted for use in config file.
23732387
"""

src/firebird/base/logging.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def process(self, msg, kwargs):
135135
self.extra['context'] = getattr(self.agent, 'log_context', None)
136136
#if "stacklevel" not in kwargs:
137137
#kwargs["stacklevel"] = 1
138-
kwargs['extra'] = self.extra
138+
kwargs['extra'] = dict(self.extra, **kwargs['extra']) if 'extra' in kwargs else self.extra
139139
return msg, kwargs
140140

141141
class LoggingManager:
@@ -344,6 +344,14 @@ def set_domain_mapping(self, domain: str, agents: Iterable[str] | str | None, *,
344344
Passing `None` to `agents` removes all agent mappings for specified domain,
345345
regardless of `replace` value.
346346
"""
347+
# Remove agents that are already mapped
348+
if agents is not None:
349+
for agent in set([agents] if isinstance(agents, str) else agents):
350+
current_domain = self._agent_domain_map.pop(agent, None)
351+
if current_domain:
352+
self._domain_agent_map[current_domain].discard(agent)
353+
if not self._domain_agent_map[current_domain]:
354+
del self._domain_agent_map[current_domain]
347355
if (replace or agents is None) and domain in self._domain_agent_map:
348356
for agent in self._domain_agent_map[domain]:
349357
del self._agent_domain_map[agent]

src/firebird/base/strconv.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def str2flag(cls: type, value: str) -> Enum:
254254
"Converts string to Enum/Flag value"
255255
result = None
256256
for item in value.lower().split('|'):
257-
value = {k.lower(): v for k, v in cls.__members__.items()}[item]
257+
value = {k.lower(): v for k, v in cls.__members__.items()}[item.strip()]
258258
if result:
259259
result |= value
260260
else:

src/firebird/base/types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,14 @@ def __repr__(self):
175175
# Distinct objects
176176
class Distinct(ABC):
177177
"""Abstract base class for classes (incl. dataclasses) with distinct instances.
178+
179+
.. important::
180+
181+
Dataclasses must be defined with `eq` set to `False`, i.e.::
182+
183+
@dataclass(eq=False)
184+
185+
Otherwise the `__hash__` and `__eq__` functions defined on `Distinct` will be overrriden.
178186
"""
179187
@abstractmethod
180188
def get_key(self) -> Hashable:
@@ -187,6 +195,10 @@ def get_key(self) -> Hashable:
187195
"""
188196
def __hash(self):
189197
return hash(self.get_key())
198+
def __eq__(self, other):
199+
if isinstance(other, Distinct):
200+
return self.get_key() == other.get_key()
201+
return False
190202
__hash__ = __hash
191203

192204
class CachedDistinctMeta(ABCMeta):
@@ -435,6 +447,7 @@ def __new__(cls, value: str):
435447
new = str.__new__(cls, value)
436448
new._callable_ = ns[callable_name]
437449
new.name = callable_name
450+
new.__doc__ = new._callable_.__doc__
438451
return new
439452
def __call__(self, *args, **kwargs):
440453
return self._callable_(*args, **kwargs)

0 commit comments

Comments
 (0)