Skip to content
Commits on Source (6)
......@@ -5,6 +5,7 @@ local-*
/html/
/venv/
/.venv/
/dist/
/build/
__pycache__/
......
......@@ -4,3 +4,4 @@ functional-python>=0.0.3
pyhocon>=0.3.51
setuptools
typing-inspect>=0.4
pytimeparse==1.1.8
......@@ -8,7 +8,7 @@ __title__ = 'dataclasses-config'
__author__ = 'Peter Zaitcev / USSX Hares'
__license__ = 'BSD 2-clause'
__copyright__ = 'Copyright 2019 Peter Zaitcev'
__version__ = '0.2.7'
__version__ = '0.3.0'
VersionInfo = namedtuple('VersionInfo', 'major minor micro releaselevel serial')
version_info = VersionInfo(*__version__.split('.'), releaselevel='alpha', serial=0)
......
import os
import warnings
from dataclasses import field
from datetime import timedelta
from importlib import import_module
from types import ModuleType
from typing import *
from dataclasses import field
from pytimeparse.timeparse import timeparse
from typing_inspect import is_generic_type, get_origin, is_optional_type, get_args
from ._decorations import *
from ._pytimeparse_patch import *
T = TypeVar('T')
def extract_optional(tp: Union[Type[Optional[T]], Type[T]]) -> Tuple[bool, Type[T]]:
......@@ -209,11 +212,60 @@ class RelPath(str):
return Path(self)
@deserialize_with(str)
class Duration(timedelta):
"""
A helper class which represents any duration.
This class is a subclass of standard timedelta.
This class' constructor accepts either:
- Single `int` or `float`: Counts this argument as the seconds
- Single `str`: Uses the special library `pytimeparse` to parse the duration
- Normal timedelta arguments (positional or keyword)
Examples:
```python
Duration(1000).total_seconds() # 1000.0
Duration('100ms').total_seconds() # 0.1
Duration('4 days 5 hours').total_seconds() # 363600.0
Duration('2:53').total_seconds() # 173.0
```
WARNING:
1. This module PATCHES the `pytimeparse` library to support milliseconds,
so it works correctly only with the version 1.1.18
and only if no other module does so.
This will be disabled as soon as the
[Feature Request #22](https://github.com/wroberts/pytimeparse/issues/22) becomes closed.
2. The `pytimeparse` library of version 1.1.8 DO NOT support
large intervals, such as YEARS and MONTHS.
This is the subject to to change.
See [Feature Request #7](https://github.com/wroberts/pytimeparse/issues/7) for details.
"""
def __new__(cls: Type['Duration'], *args, **kwargs):
if (len(args) == 1 and not kwargs):
arg = args[0]
if (isinstance(arg, timedelta)):
return super().__new__(cls, seconds=arg.total_seconds())
elif (isinstance(arg, str)):
return super().__new__(cls, seconds=timeparse(arg))
elif (isinstance(arg, (int, float))):
return super().__new__(cls, seconds=arg)
return super().__new__(cls, *args, **kwargs)
__all__ = \
[
'extract_generic',
'extract_optional',
'Duration',
'DynamicClass',
'Path',
'RelPath',
......
import re
from pytimeparse import timeparse
timeparse.MILLIS = r'(?P<millis>[\d.]+)\s*(?:ms|msecs?|millis|milliseconds?)'
timeparse.TIMEFORMATS[0] += r'\s*' + timeparse.OPT(timeparse.MILLIS)
timeparse.MULTIPLIERS['millis'] = 1e-3
timeparse.COMPILED_TIMEFORMATS[0] = re.compile(r'\s*' + timeparse.TIMEFORMATS[0] + r'\s*$', re.I)
__all__ = [ ]
from .classes_tests import *
from .durations_test import *
from dataclasses import dataclass
from datetime import timedelta
from unittest import TestCase, main
from dataclasses_config import *
@dataclass(frozen=Settings.frozen)
class NestedConfig(Config):
dur3: Duration
@main_config(log_config=True, reference_config_name='dur.conf')
class DurationConfig(MainConfig):
dur: Duration
dur2: Duration
nested: NestedConfig
class DurationsTestCase(TestCase):
def assertDuration(self, duration: Duration, expected_total_seconds: float):
self.assertIs(type(duration), Duration)
self.assertIsInstance(duration, timedelta)
self.assertEqual(duration.total_seconds(), expected_total_seconds)
def test_parse_time(self):
params = \
[
(timedelta(seconds=15), 15.0),
(Duration(hours=6), 21600.0),
(timedelta(hours=6), 21600.0),
(1000, 1000.0),
('100ms', 0.1),
('100 millis', 0.1),
('1 millisecond', 0.001),
('2:53', 173.0),
('2:53:58', 10438.0),
('4 hours 51 minutes 13 milliseconds', 17460.013),
('4 days 5 hours', 363600.0),
('31 weeks', 18748800.0),
]
for i, (arg, expected) in enumerate(params, start=1):
with self.subTest(f"Test #{i}", arg=arg):
self.assertDuration(Duration(arg), expected)
def test_timedelta_keyword_constructor(self):
params = \
[
(dict(minutes=2, seconds=53), 173.0),
(dict(hours=2, minutes=53, seconds=58), 10438.0),
(dict(hours=4, minutes=51, milliseconds=13), 17460.013),
(dict(days=4, hours=5), 363600.0),
(dict(weeks=31), 18748800.0),
]
for i, (arg, expected) in enumerate(params, start=1):
with self.subTest(f"Test #{i}", **arg):
self.assertDuration(Duration(**arg), expected)
def test_timedelta_deserialization(self):
conf = DurationConfig.default()
self.assertDuration(conf.dur, 5.0)
self.assertDuration(conf.dur2, 0.015)
self.assertDuration(conf.nested.dur3, 21600.0)
__all__ = \
[
'DurationsTestCase',
]
if (__name__ == '__main__'):
exit_code = main()
exit(exit_code)
dur: 5s
dur2: 15 ms
nested.dur3 = 6 hours