tree.pyx 38.8 KB
Newer Older
1 2
# distutils:language=c++
# cython:language_level=3
HansBug's avatar
HansBug 已提交
3

4
import os
HansBug's avatar
HansBug 已提交
5
from collections.abc import Sized, Container, Reversible, Mapping
6
from operator import itemgetter
7

8
import cython
9
from hbutils.design import SingletonMark
HansBug's avatar
HansBug 已提交
10

HansBug's avatar
HansBug 已提交
11
from .constraint cimport Constraint, to_constraint, transact, _EMPTY_CONSTRAINT
HansBug's avatar
HansBug 已提交
12
from ..common.delay cimport undelay, _c_delayed_partial, DelayedProxy
13
from ..common.storage cimport TreeStorage, create_storage, _c_undelay_data
14
from ...utils import format_tree
HansBug's avatar
HansBug 已提交
15

16 17 18
cdef class _CObject:
    pass

19 20 21 22 23 24 25
try:
    reversed({'a': 1}.keys())
except TypeError:
    _reversible = False
else:
    _reversible = True

26 27 28 29 30 31
cdef inline object _item_unwrap(object v):
    if isinstance(v, list) and len(v) == 1:
        return v[0]
    else:
        return v

32 33
_GET_NO_DEFAULT = SingletonMark('get_no_default')

34 35 36 37
cdef inline TreeStorage _dict_unpack(dict d):
    cdef str k
    cdef object v
    cdef dict result = {}
HansBug's avatar
HansBug 已提交
38

39 40 41 42 43
    for k, v in d.items():
        if isinstance(v, dict):
            result[k] = _dict_unpack(v)
        elif isinstance(v, TreeValue):
            result[k] = v._detach()
HansBug's avatar
HansBug 已提交
44
        else:
45
            result[k] = v
HansBug's avatar
HansBug 已提交
46

47
    return create_storage(result)
HansBug's avatar
HansBug 已提交
48

49 50
_DEFAULT_STORAGE = create_storage({})

51 52 53 54 55 56 57 58 59 60
cdef class _SimplifiedConstraintProxy:
    def __cinit__(self, Constraint cons):
        self.cons = cons

cdef inline Constraint _c_get_constraint(object cons):
    if isinstance(cons, _SimplifiedConstraintProxy):
        return cons.cons
    else:
        return to_constraint(cons)

61 62 63 64 65 66 67 68 69 70 71 72
cdef class ValidationError(Exception):
    def __init__(self, TreeValue obj, Exception error, tuple path, Constraint cons):
        Exception.__init__(self, obj, error, path, cons)
        self._object = obj
        self._error = error
        self._path = path
        self._cons = cons

    def __str__(self):
        return f"Validation failed on {self._cons!r} at position {self._path!r}{os.linesep}" \
               f"{type(self._error).__name__}: {self._error}"

73 74
cdef class TreeValue:
    r"""
75 76 77 78 79 80 81
    Overview:
        Base framework of tree value. \
        And if the fast functions and operators are what you need, \
        please use `FastTreeValue` in `treevalue.tree.general`. \
        The `TreeValue` class is a light-weight framework just for DIY.
    """

82
    def __cinit__(self, object data, object constraint=None):
83
        self._st = _DEFAULT_STORAGE
HansBug's avatar
HansBug 已提交
84
        self.constraint = _EMPTY_CONSTRAINT
85
        self._type = type(self)
86
        self._child_constraints = {}
87

88
    @cython.binding(True)
HansBug's avatar
HansBug 已提交
89
    def __init__(self, object data, object constraint=None):
90
        """
91
        Constructor of :class:`TreeValue`.
92

93 94
        :param data: Original data to init a tree value, should be a :class:`treevalue.tree.common.TreeStorage`, \
            :class:`TreeValue` or a :class:`dict`.
95 96

        Example:
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
            >>> from treevalue import TreeValue
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})  # create an instance
            >>> t
            <TreeValue 0x7fbcf70750b8>
            ├── 'a' --> 1
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7fbcf70750f0>
                ├── 'c' --> 3
                └── 'd' --> 4
            >>> t2 = TreeValue(t)  # create a new treevalue with the same storage of t
            >>> t2
            <TreeValue 0x7fbcf70750b8>
            ├── 'a' --> 1
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7fbcf70750f0>
                ├── 'c' --> 3
                └── 'd' --> 4
            >>> t2 is t
            False
116
        """
117 118
        if isinstance(data, TreeStorage):
            self._st = data
119
            self.constraint = _c_get_constraint(constraint)
120 121
        elif isinstance(data, TreeValue):
            self._st = data._detach()
122 123
            if constraint is None:
                self.constraint = data.constraint
124
                self._child_constraints = data._child_constraints
125
            else:
126
                self.constraint = _c_get_constraint(constraint)
127 128
        elif isinstance(data, dict):
            self._st = _dict_unpack(data)
129
            self.constraint = _c_get_constraint(constraint)
130 131 132 133
        else:
            raise TypeError(
                "Unknown initialization type for tree value - {type}.".format(
                    type=repr(type(data).__name__)))
HansBug's avatar
HansBug 已提交
134

135 136 137
    def __getnewargs_ex__(self):  # for __cinit__, when pickle.loads
        return ({},), {}

138
    @cython.binding(True)
139
    cpdef TreeStorage _detach(self):
140 141 142 143 144 145 146 147 148 149
        """
        Detach the inner :class:`treevalue.tree.common.TreeStorage` object.

        :return: :class:`treevalue.tree.common.TreeStorage` object.

        .. warning::
            This is an inner method of :class:`TreeValue`, which is not recommended to be actually used. \
            If you need to instantiate a new :class:`TreeValue` with the same storage, just pass this \
            :class:`TreeValue` object as the argument of the :meth:`__init__`.
        """
150 151
        return self._st

152
    cdef inline object _unraw(self, object obj, str key):
153
        cdef _SimplifiedConstraintProxy child_constraint
154
        if isinstance(obj, TreeStorage):
155 156 157 158 159 160
            if key in self._child_constraints:
                child_constraint = self._child_constraints[key]
            else:
                child_constraint = _SimplifiedConstraintProxy(transact(self.constraint, key))
                self._child_constraints[key] = child_constraint
            return self._type(obj, constraint=child_constraint)
161 162 163
        else:
            return obj

HansBug's avatar
HansBug 已提交
164
    cdef inline object _raw(self, object obj):
165 166 167 168
        if isinstance(obj, TreeValue):
            return obj._detach()
        else:
            return obj
HansBug's avatar
HansBug 已提交
169

170
    @cython.binding(True)
171
    cpdef get(self, str key, object default=None):
172
        r"""
173
        Get item from the tree node.
174

175 176 177
        :param key: Item's name.
        :param default: Default value when this item is not found, default is ``None``.
        :return: Item's value.
178

179 180 181
        .. note::
            The method :meth:`get` will never raise ``KeyError``, like the behaviour in \
            `dict.get <https://docs.python.org/3/library/stdtypes.html#dict.get>`_.
182 183 184 185 186 187 188 189 190 191 192 193 194 195

        Examples:
            >>> from treevalue import TreeValue
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> t.get('a')
            1
            >>> t.get('x')
            <TreeValue 0x7f488a65f0b8>
            ├── 'c' --> 3
            └── 'd' --> 4
            >>> t.get('f')  # key not exist
            None
            >>> t.get('f', 123)
            123
196
        """
197
        return self._unraw(self._st.get_or_default(key, default), key)
198

HansBug's avatar
HansBug 已提交
199 200 201
    @cython.binding(True)
    cpdef pop(self, str key, object default=_GET_NO_DEFAULT):
        """
202
        Pop item from the tree node.
HansBug's avatar
HansBug 已提交
203

204 205 206 207
        :param key: Item's name.
        :param default: Default value when this item is not found, default is ``_GET_NO_DEFAULT`` which means \
            just raise ``KeyError`` when not found.
        :return: Item's value.
208
        :raise KeyError: When ``key`` is not exist and ``default`` is not given, raise ``KeyError``.
HansBug's avatar
HansBug 已提交
209

210 211 212
        .. note::
            The method :meth:`pop` will raise ``KeyError`` when ``key`` is not found, like the behaviour in \
            `dict.pop <https://docs.python.org/3/library/stdtypes.html#dict.pop>`_.
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
        
        Examples:
            >>> from treevalue import TreeValue
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> t.pop('a')
            1
            >>> t.pop('x')
            <TreeValue 0x7f488a65f080>
            ├── 'c' --> 3
            └── 'd' --> 4
            >>> t
            <TreeValue 0x7f488a65f048>
            └── 'b' --> 2
            >>> t.pop('f')
            KeyError: 'f'
            >>> t.pop('f', 123)
            123
HansBug's avatar
HansBug 已提交
230 231 232 233 234 235 236
        """
        cdef object value
        if default is _GET_NO_DEFAULT:
            value = self._st.pop(key)
        else:
            value = self._st.pop_or_default(key, default)

237
        return self._unraw(value, key)
HansBug's avatar
HansBug 已提交
238

HansBug's avatar
HansBug 已提交
239 240 241
    @cython.binding(True)
    cpdef popitem(self):
        """
242
        Pop item (with a key and its value) from the tree node.
HansBug's avatar
HansBug 已提交
243 244 245 246 247 248 249

        :return: Popped item.
        :raise KeyError: When current treevalue is empty.

        .. note::
            The method :meth:`popitem` will raise ``KeyError`` when empty, like the behaviour in \
            `dict.popitem <https://docs.python.org/3/library/stdtypes.html#dict.popitem>`_.
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
        
        Examples:
            >>> from treevalue import TreeValue
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> t.popitem()
            ('x', <TreeValue 0x7f488a65f0b8>
            ├── 'c' --> 3
            └── 'd' --> 4
            )
            >>> t.popitem()
            ('b', 2)
            >>> t.popitem()
            ('a', 1)
            >>> t.popitem()
            KeyError: 'popitem(): TreeValue is empty.'
HansBug's avatar
HansBug 已提交
265 266 267 268 269
        """
        cdef str k
        cdef object v
        try:
            k, v = self._st.popitem()
270
            return k, self._unraw(v, k)
HansBug's avatar
HansBug 已提交
271 272 273
        except KeyError:
            raise KeyError(f'popitem(): {self._type.__name__} is empty.')

274 275 276
    @cython.binding(True)
    cpdef void clear(self):
        """
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
        Clear all the items in this treevalue object.
        
        Examples:
            >>> from treevalue import TreeValue
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> t
            <TreeValue 0x7fd2553f0048>
            ├── 'a' --> 1
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7fd2553e3fd0>
                ├── 'c' --> 3
                └── 'd' --> 4
            >>> t.clear()
            >>> t
            <TreeValue 0x7fd2553f0048>
292 293 294
        """
        self._st.clear()

295 296 297
    @cython.binding(True)
    cpdef object setdefault(self, str key, object default=None):
        """
298 299
        Set the ``default`` to this treevalue and return it if the ``key`` is not exist, \
        otherwise just return the existing value of ``key``.
300 301 302 303 304 305 306 307 308

        :param key: Items' name.
        :param default: Default value of the ``key``, ``None`` will be used when not given.
        :return: The newest value of the ``key``.

        .. note::
            The behaviour of method :meth:`setdefault` is similar to \
            `dict.setdefault <https://docs.python.org/3/library/stdtypes.html#dict.setdefault>`_.

309
        Examples:
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
            >>> from treevalue import TreeValue, delayed
            >>> t = TreeValue({'a': 1, 'b': 3, 'c': '233'})
            >>> t.setdefault('d', 'dsflgj')  # set new value
            'dsflgj'
            >>> t
            <TreeValue 0x7efe31576048>
            ├── 'a' --> 1
            ├── 'b' --> 3
            ├── 'c' --> '233'
            └── 'd' --> 'dsflgj'
            >>> t.setdefault('ff')  # default value - None
            >>> t
            <TreeValue 0x7efe31576048>
            ├── 'a' --> 1
            ├── 'b' --> 3
            ├── 'c' --> '233'
            ├── 'd' --> 'dsflgj'
            └── 'ff' --> None
            >>> t.setdefault('a', 1000)  # existing key
            1
            >>> t
            <TreeValue 0x7efe31576048>
            ├── 'a' --> 1
            ├── 'b' --> 3
            ├── 'c' --> '233'
            ├── 'd' --> 'dsflgj'
            └── 'ff' --> None
            >>> t.setdefault('g', delayed(lambda: 1))  # delayed value
            1
            >>> t
            <TreeValue 0x7efe31576048>
            ├── 'a' --> 1
            ├── 'b' --> 3
            ├── 'c' --> '233'
            ├── 'd' --> 'dsflgj'
            ├── 'ff' --> None
            └── 'g' --> 1
        """
348
        return self._unraw(self._st.setdefault(key, self._raw(default)), key)
349

350
    cdef inline void _update(self, object d, dict kwargs) except*:
HansBug's avatar
HansBug 已提交
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
        cdef object dt
        if d is None:
            dt = {}
        elif isinstance(d, Mapping):
            dt = d
        elif isinstance(d, TreeValue):
            dt = d._detach().detach()
        elif isinstance(d, TreeStorage):
            dt = d.detach()
        else:
            raise TypeError(f'Invalid type of update dict - {type(d)!r}.')

        cdef str key
        cdef object value
        for key, value in dt.items():
            self._st.set(key, self._raw(value))
        for key, value in kwargs.items():
            self._st.set(key, self._raw(value))

    @cython.binding(True)
    def update(self, __update_dict=None, **kwargs):
        """
        Overview:
            Update items in current treevalue.

        :param __update_dict: Dictionary object for updating.
        :param kwargs: Arguments for updating.

        .. note::
            The method :meth:`update` is similar to \
            `dict.update <https://docs.python.org/3/library/stdtypes.html#dict.update>`_.

383
        Examples:
HansBug's avatar
HansBug 已提交
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
            >>> from treevalue import TreeValue
            >>> t = TreeValue({'a': 1, 'b': 3, 'c': '233'})
            >>> t.update({'a': 10, 'f': 'sdkj'})  # with dict
            >>> t
            <TreeValue 0x7fa31f5ba048>
            ├── 'a' --> 10
            ├── 'b' --> 3
            ├── 'c' --> '233'
            └── 'f' --> 'sdkj'
            >>> t.update(a=100, ft='fffff')  # with key-word arguments
            >>> t
            <TreeValue 0x7fa31f5ba048>
            ├── 'a' --> 100
            ├── 'b' --> 3
            ├── 'c' --> '233'
            ├── 'f' --> 'sdkj'
            └── 'ft' --> 'fffff'
            >>> t.update(TreeValue({'f': {'x': 1}, 'b': 40}))  # with TreeValue
            >>> t
            <TreeValue 0x7fa31f5ba048>
            ├── 'a' --> 100
            ├── 'b' --> 40
            ├── 'c' --> '233'
            ├── 'f' --> <TreeValue 0x7fa31f5ba278>
            │   └── 'x' --> 1
            └── 'ft' --> 'fffff'
        """
        self._update(__update_dict, kwargs)

413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428
    @cython.binding(True)
    cpdef _attr_extern(self, str key):
        r"""
        Overview:
            External protected function for support the unfounded attributes. \
            Default is raise a `KeyError`.

        Arguments:
            - key (:obj:`str`): Attribute name.

        Returns:
            - return (:obj:): Anything you like, \
                and if it is not able to validly return anything, \
                just raise an exception here.
        """
        raise AttributeError("Attribute {key} not found.".format(key=repr(key)))
429

430
    @cython.binding(True)
431
    def __getattribute__(self, str item):
432
        """
433
        Get item from this tree value.
434

435 436
        :param item: Name of attribute.
        :return: Target attribute value; if ``item`` is exist in :meth:`keys`, return its value.
437 438

        Example:
439
            >>> from treevalue import TreeValue
440
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
441 442 443 444 445 446 447 448 449 450 451 452 453 454
            >>> t.a
            1
            >>> t.b
            2
            >>> t.x
            <TreeValue 0x7fd2553e3fd0>
            ├── 'c' --> 3
            └── 'd' --> 4
            >>> t.x.c
            3
            >>> t.keys  # this is a method
            <bound method TreeValue.keys of <TreeValue 0x7fd2553f00b8>>
            >>> t.ff  # this key is not exist
            AttributeError: Attribute 'ff' not found.
455
        """
456

457
        # the order of the attributes' getting is altered
458 459 460
        # original order: __dict__, self._st, self._attr_extern
        # new order: self._st, __dict__, self._attr_extern
        # this may cause problem when pickle.loads, so __getnewargs_ex__ and __cinit__ is necessary
461
        if self._st.contains(item):
462
            return self._unraw(self._st.get(item), item)
463 464 465 466 467
        else:
            try:
                return object.__getattribute__(self, item)
            except AttributeError:
                return self._attr_extern(item)
HansBug's avatar
HansBug 已提交
468

469 470
    @cython.binding(True)
    def __setattr__(self, str key, object value):
471
        """
472
        Set sub node to this tree value.
473

474 475
        :param key: Name of the attribute.
        :param value: Sub value.
476 477

        Example:
478
            >>> from treevalue import TreeValue
479
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
            >>> t.a = 3  # set a value
            >>> t
            <TreeValue 0x7fd2553f0048>
            ├── 'a' --> 3
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7fd2553f0080>
                ├── 'c' --> 3
                └── 'd' --> 4
            >>> t.b = {'x': 1, 'y': 2}  # set a dict
            >>> t
            <TreeValue 0x7fd2553f0048>
            ├── 'a' --> 3
            ├── 'b' --> {'x': 1, 'y': 2}
            └── 'x' --> <TreeValue 0x7fd2553f0080>
                ├── 'c' --> 3
                └── 'd' --> 4
            >>> t.b = TreeValue({'x': 1, 'y': 2})  # set a tree
            >>> t
            <TreeValue 0x7fd2553f0048>
            ├── 'a' --> 3
            ├── 'b' --> <TreeValue 0x7fd2553e3fd0>
            │   ├── 'x' --> 1
            │   └── 'y' --> 2
            └── 'x' --> <TreeValue 0x7fd2553f0080>
                ├── 'c' --> 3
                └── 'd' --> 4
506
        """
507
        self._st.set(key, self._raw(value))
HansBug's avatar
HansBug 已提交
508

509 510
    @cython.binding(True)
    def __delattr__(self, str item):
511
        """
512
        Delete attribute from tree value.
513

514
        :param item: Name of the attribute.
515 516

        Example:
517
            >>> from treevalue import TreeValue
518
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
519 520 521 522 523 524 525 526 527 528 529 530 531
            >>> del t.a
            >>> t
            <TreeValue 0x7fd2251ae320>
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7fd2553f00b8>
                ├── 'c' --> 3
                └── 'd' --> 4
            >>> del t.x.c
            >>> t
            <TreeValue 0x7fd2251ae320>
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7fd2553f00b8>
                └── 'd' --> 4
532
        """
533 534 535
        try:
            self._st.del_(item)
        except KeyError:
536
            raise AttributeError(f"Unable to delete attribute {item!r}.")
HansBug's avatar
HansBug 已提交
537

538 539 540
    @cython.binding(True)
    cpdef _getitem_extern(self, object key):
        r"""
541 542
        External protected function for support the :meth:`__getitem__` operation. \
        Default behaviour is raising a `KeyError`.
543

544 545 546
        :param key: Item object.
        :return: Anything you like, this depends on your implements. 
        :raise KeyError: If it is not able to validly return anything, just raise an ``KeyError`` here.
547
        """
548
        raise KeyError(key)
549 550 551 552

    @cython.binding(True)
    def __getitem__(self, object key):
        """
553
        Get item from this tree value.
554

555 556
        :param key: Item object.
        :return: Target object value.
557 558

        Example:
559
            >>> from treevalue import TreeValue
560
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
561 562 563 564 565 566
            >>> t['a']
            1
            >>> t['b']
            2
            >>> t['x']['c']
            3
567
        """
568
        if isinstance(key, str):
569
            return self._unraw(self._st.get(key), key)
570
        else:
571
            return self._getitem_extern(_item_unwrap(key))
572 573 574 575

    @cython.binding(True)
    cpdef _setitem_extern(self, object key, object value):
        r"""
576
        External function for supporting :meth:`__setitem__` operation.
577
        
578 579 580
        :param key: Key object.
        :param value: Value object.
        :raise NotImplementedError: When not implemented, raise ``NotImplementedError``. 
581 582 583 584 585 586
        """
        raise NotImplementedError

    @cython.binding(True)
    def __setitem__(self, object key, object value):
        """
587
        Set item to current :class:`TreeValue` object.
588

589 590
        :param key: Key object.
        :param value: Value object.
591

592
        Examples:
593
            >>> from treevalue import TreeValue
594 595 596 597
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> t['a'] = 11
            >>> t['x']['c'] = 30
            >>> t
598 599 600 601 602 603
            <TreeValue 0x7f11704c5358>
            ├── 'a' --> 11
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7f11704c52e8>
                ├── 'c' --> 30
                └── 'd' --> 4
604 605 606 607
        """
        if isinstance(key, str):
            self._st.set(key, self._raw(value))
        else:
608
            self._setitem_extern(_item_unwrap(key), value)
609 610 611 612

    @cython.binding(True)
    cpdef _delitem_extern(self, object key):
        r"""
613 614 615 616
        External function for supporting :meth:`__delitem__` operation.

        :param key: Key object.
        :raise KeyError: Raise ``KeyError`` in default case.
617
        """
618
        raise KeyError(key)
619 620 621 622

    @cython.binding(True)
    def __delitem__(self, object key):
        """
623
        Delete item from current :class:`TreeValue`.
624

625
        :param key: Key object.
626

627
        Examples:
628 629 630 631 632 633 634 635 636
            >>> from treevalue import TreeValue
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> del t['a']
            >>> del t['x']['c']
            >>> t
            <TreeValue 0x7f11704c53c8>
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7f11704c5438>
                └── 'd' --> 4
637
        """
638
        if isinstance(key, str):
639 640
            self._st.del_(key)
        else:
641
            self._delitem_extern(_item_unwrap(key))
642

643
    @cython.binding(True)
644
    def __contains__(self, str key):
645
        """
646
        Check if attribute is in this tree value.
647

648 649
        :param key: Key to check.
        :return: ``key`` is existed or not in this tree value.
650 651

        Example:
652
            >>> from treevalue import TreeValue
653 654 655 656 657
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> 'a' in t  # True
            >>> 'b' in t  # True
            >>> 'c' in t  # False
        """
658
        return self._st.contains(key)
HansBug's avatar
HansBug 已提交
659

660
    @cython.binding(True)
661
    def __iter__(self):
662
        """
663
        Get iterator of this tree value.
664

665
        :return: An iterator for the keys.
666

667 668
        Examples:
            >>> from treevalue import TreeValue
669
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': 3})
670 671 672 673 674
            >>> for key in t:
            ...     print(key)
            a
            b
            x
675

676 677 678 679 680
        .. note::
            The method :meth:`__iter__`'s bahaviour should be similar to \
            `dict.__iter__ <https://docs.python.org/3/library/stdtypes.html#dict.update>`_.
        """
        yield from self._st.iter_keys()
681

682 683
    @cython.binding(True)
    def __reversed__(self):
684
        """
685
        Get the reversed iterator of tree value.
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704

        :return: A reversed iterator for the keys.

        Examples:
            >>> from treevalue import TreeValue
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': 3})
            >>> for key in reversed(t):
            ...     print(key)
            x
            b
            a

        .. note::
            Only available in python 3.8 or higher version.
        """
        if _reversible:
            return self._st.iter_rev_keys()
        else:
            raise TypeError(f'{self._type.__name__!r} object is not reversible')
705

706
    @cython.binding(True)
HansBug's avatar
HansBug 已提交
707
    def __len__(self):
708
        """
709
        Get count of the keys.
710

711
        :return: Count of the keys.
712 713

        Example:
714
            >>> from treevalue import TreeValue
715 716 717 718
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> len(t)    # 3
            >>> len(t.x)  # 2
        """
719
        return self._st.size()
HansBug's avatar
HansBug 已提交
720

721 722
    @cython.binding(True)
    def __bool__(self):
723
        """
724
        Check if the tree value is not empty.
725

726
        :return: Not empty or do.
727 728

        Example:
729
            >>> from treevalue import TreeValue
730 731 732 733 734
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}, 'e': {}})
            >>> not not t    # True
            >>> not not t.x  # True
            >>> not not t.e  # False
        """
735 736 737 738
        return not self._st.empty()

    @cython.binding(True)
    def __repr__(self):
739
        """
740
        Get representation format of tree value.
741

742
        :return: Represenation string.
743 744 745

        Example:
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
746 747 748 749 750 751 752
            >>> t
            <TreeValue 0x7f672fc53320>
            ├── 'a' --> 1
            ├── 'b' --> 2
            └── 'x' --> <TreeValue 0x7f672fc53390>
                ├── 'c' --> 3
                └── 'd' --> 4
753
        """
754 755 756 757
        return format_tree(
            _build_tree(self._detach(), self._type, '', {}, ()),
            itemgetter(0), itemgetter(1),
        )
HansBug's avatar
HansBug 已提交
758

759
    @cython.binding(True)
HansBug's avatar
HansBug 已提交
760
    def __hash__(self):
761
        """
762
        Hash value of current object.
763

764 765 766 767 768
        :return: Hash value of current object.

        .. note::
            If the structure and hash values of two tree values are exactly the same, their hash value \
            is guaranteed to be the same.
769
        """
770
        return hash(self._st)
HansBug's avatar
HansBug 已提交
771

772 773
    @cython.binding(True)
    def __eq__(self, object other):
774
        """
775
        Check the equality of two tree values.
776

777 778
        :param other: Another tree value object.
        :return: Equal or not.
779 780

        Example:
781
            >>> from treevalue import TreeValue, clone, FastTreeValue
782 783
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})
            >>> t == TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})      # True
784
            >>> t == TreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 5}})      # False
785 786
            >>> t == FastTreeValue({'a': 1, 'b': 2, 'x': {'c': 3, 'd': 4}})  # False (type not match)
        """
HansBug's avatar
HansBug 已提交
787 788
        if self is other:
            return True
789 790
        elif type(other) == self._type:
            return self._st == other._detach()
HansBug's avatar
HansBug 已提交
791 792 793
        else:
            return False

794
    @cython.binding(True)
795
    def __setstate__(self, tuple state):
HansBug's avatar
HansBug 已提交
796
        """
797
        Deserialize operation, can support `pickle.loads`.
HansBug's avatar
HansBug 已提交
798

799 800
        :param state: :class:`treevalue.tree.common.TreeStorage` object, \
            which should be used when deserialization.
HansBug's avatar
HansBug 已提交
801 802 803 804 805 806 807 808

        Examples:
            >>> import pickle
            >>> from treevalue import TreeValue
            >>>
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3}})
            >>> bin_ = pickle.dumps(t)  # dump it to binary
            >>> pickle.loads(bin_)      #  TreeValue({'a': 1, 'b': 2, 'x': {'c': 3}})
HansBug's avatar
HansBug 已提交
809
        """
810
        self._st, self.constraint = state
HansBug's avatar
HansBug 已提交
811

812
    @cython.binding(True)
HansBug's avatar
HansBug 已提交
813 814
    def __getstate__(self):
        """
815 816 817
        Serialize operation, can support `pickle.dumps`.

        :return: A :class:`treevalue.tree.common.TreeStorage` object, used for serialization.
HansBug's avatar
HansBug 已提交
818 819 820 821 822 823 824 825

        Examples:
            >>> import pickle
            >>> from treevalue import TreeValue
            >>>
            >>> t = TreeValue({'a': 1, 'b': 2, 'x': {'c': 3}})
            >>> bin_ = pickle.dumps(t)  # dump it to binary
            >>> pickle.loads(bin_)      #  TreeValue({'a': 1, 'b': 2, 'x': {'c': 3}})
HansBug's avatar
HansBug 已提交
826
        """
827
        return self._st, self.constraint
828

829
    @cython.binding(True)
830
    cpdef treevalue_keys keys(self):
831
        """
832
        Get keys of this treevalue object, like the :class:`dict`.
833

834
        :return: A mapping object for tree value's keys. 
835

836
        Examples:
837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854
            >>> from treevalue import TreeValue
            >>> 
            >>> t = TreeValue({'a': 1, 'b': 3, 'c': '233'})
            >>> t.keys()
            treevalue_keys(['a', 'b', 'c'])
            >>> len(t.keys())
            3
            >>> list(t.keys())
            ['a', 'b', 'c']
            >>> list(reversed(t.keys()))  # only available in python3.8+
            ['c', 'b', 'a']
            >>> 'a' in t.keys()
            True
            >>> 'f' in t.keys()
            False

        .. note::
            :func:`reversed` is only available in python 3.8 or higher versions.
855
        """
856
        return treevalue_keys(self, self._st)
857 858

    @cython.binding(True)
859
    cpdef treevalue_values values(self):
860
        """
861
        Get value of this treevalue object, like the :class:`dict`.
862

863
        :return: A mapping object for tree value's values.
864

865
        Examples:
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883
            >>> from treevalue import TreeValue
            >>> 
            >>> t = TreeValue({'a': 1, 'b': 3, 'c': '233'})
            >>> t.values()
            treevalue_values([1, 3, '233'])
            >>> len(t.values())
            3
            >>> list(t.values())
            [1, 3, '233']
            >>> list(reversed(t.values()))  # only supported on python3.8+
            ['233', 3, 1]
            >>> 1 in t.values()
            True
            >>> 'fff' in t.values()
            False

        .. note::
            :func:`reversed` is only available in python 3.8 or higher versions.
884
        """
885
        return treevalue_values(self, self._st)
886 887

    @cython.binding(True)
888
    cpdef treevalue_items items(self):
889
        """
890
        Get pairs of keys and values of this treevalue object, like the :class:`items`.
891

892
        :return: A mapping object for tree value's items.
893

894
        Examples:
895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912
            >>> from treevalue import TreeValue
            >>> 
            >>> t = TreeValue({'a': 1, 'b': 3, 'c': '233'})
            >>> t.items()
            treevalue_items([('a', 1), ('b', 3), ('c', '233')])
            >>> len(t.items())
            3
            >>> list(t.items())
            [('a', 1), ('b', 3), ('c', '233')]
            >>> list(reversed(t.items()))  # only supported on python3.8+
            [('c', '233'), ('b', 3), ('a', 1)]
            >>> ('a', 1) in t.items()
            True
            >>> ('c', '234') in t.values()
            False

        .. note::
            :func:`reversed` is only available in python 3.8 or higher versions.
913
        """
914
        return treevalue_items(self, self._st)
915

HansBug's avatar
HansBug 已提交
916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
    cdef tuple _unpack(self, tuple keys, object default=_GET_NO_DEFAULT):
        cdef object key
        cdef list result = []
        for key in keys:
            if self._st.contains(key) or default is not _GET_NO_DEFAULT:
                result.append(self.get(key, default))
            else:
                raise KeyError(f'Key not found - {key!r}.')

        return tuple(result)

    @cython.binding(True)
    def unpack(self, *keys, default=_GET_NO_DEFAULT):
        """
        Unpack values from current treevalue object.

        :param keys: Keys to unpack.
        :param default: Default value when key not found. Raise `KeyError` when not given.

        Examples::

            >>> from treevalue import FastTreeValue
            >>> t1 = FastTreeValue({
            ...     'a': 21, 'b': {'x': 'f-49', 'y': 7.7},
            ... })
            >>>
            >>> t1.unpack('a', 'b')
            (21, <FastTreeValue 0x7ffb77fc2850>
            ├── 'x' --> 'f-49'
            └── 'y' --> 7.7
            )
            >>> t1.b.unpack('x', 'y')
            ('f-49', 7.7)
            >>> t1.b.unpack('x', 'y', 'z')
            KeyError: "Key not found - 'z'."
            >>> t1.b.unpack('x', 'y', 'z', default=None)
            ('f-49', 7.7, None)
        """
        return self._unpack(keys, default)

956 957 958 959 960 961 962 963 964 965 966 967
    @cython.binding(True)
    cpdef void validate(self) except*:
        cdef bool retval
        cdef tuple retpath
        cdef Constraint retcons
        cdef Exception reterr

        if __debug__:
            retval, retpath, retcons, reterr = self.constraint.check(self)
            if not retval:
                raise ValidationError(self, reterr, retpath, retcons)

968 969 970
    @cython.binding(True)
    def with_constraints(self, object constraint, bool clear=False):
        if clear:
971
            return self._type(self._st, constraint=to_constraint(constraint))
972
        else:
973
            return self._type(self._st, constraint=to_constraint([constraint, self.constraint]))
974

HansBug's avatar
HansBug 已提交
975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991
    @cython.final
    cdef inline object _get_tree_graph(self):
        from .graph import graphics
        return graphics((self, '<root>'))

    @cython.binding(True)
    def _repr_svg_(self):
        return self._get_tree_graph().pipe(format='svg', encoding='utf-8')

    @cython.binding(True)
    def _repr_png_(self):
        return self._get_tree_graph().pipe(format='png')

    @cython.binding(True)
    def _repr_jpeg_(self):
        return self._get_tree_graph().pipe(format='jpeg')

992 993 994 995 996 997 998 999 1000 1001
cdef str _prefix_fix(object text, object prefix):
    cdef list lines = []
    cdef int i
    cdef str line
    cdef str white = ' ' * len(prefix)
    for i, line in enumerate(text.splitlines()):
        lines.append((prefix if i == 0 else white) + line)

    return os.linesep.join(lines)

1002 1003 1004
cdef inline str _title_repr(TreeStorage st, object type_):
    return f'<{type_.__name__} {hex(id(st))}>'

1005 1006
cdef object _build_tree(TreeStorage st, object type_, str prefix, dict id_pool, tuple path):
    cdef object nid = id(st)
1007
    cdef str self_repr = _title_repr(st, type_)
1008 1009 1010
    cdef list children = []

    cdef str k, _t_prefix
HansBug's avatar
HansBug 已提交
1011
    cdef object v, nv
1012 1013 1014 1015 1016 1017 1018 1019 1020
    cdef dict data
    cdef tuple curpath
    if nid in id_pool:
        self_repr = os.linesep.join([
            self_repr, f'(The same address as {".".join(("<root>", *id_pool[nid]))})'])
    else:
        id_pool[nid] = path
        data = st.detach()
        for k, v in sorted(data.items()):
1021
            v = _c_undelay_data(data, k, v)
1022
            curpath = path + (k,)
1023
            _t_prefix = f'{repr(k)} --> '
1024 1025 1026 1027 1028 1029 1030
            if isinstance(v, TreeStorage):
                children.append(_build_tree(v, type_, _t_prefix, id_pool, curpath))
            else:
                children.append((_prefix_fix(repr(v), _t_prefix), []))

    self_repr = _prefix_fix(self_repr, prefix)
    return self_repr, children
HansBug's avatar
HansBug 已提交
1031

1032 1033
# noinspection PyPep8Naming
cdef class treevalue_keys(_CObject, Sized, Container, Reversible):
1034
    def __cinit__(self, TreeValue tv, TreeStorage storage):
1035
        self._st = storage
1036
        self._type = type(tv)
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065

    def __len__(self):
        return self._st.size()

    def __contains__(self, item):
        return self._st.contains(item)

    def _iter(self):
        for k in self._st.iter_keys():
            yield k

    def __iter__(self):
        return self._iter()

    def _rev_iter(self):
        for k in self._st.iter_rev_keys():
            yield k

    def __reversed__(self):
        if _reversible:
            return self._rev_iter()
        else:
            raise TypeError(f'{type(self).__name__!r} object is not reversible')

    def __repr__(self):
        return f'{type(self).__name__}({list(self)!r})'

# noinspection PyPep8Naming
cdef class treevalue_values(_CObject, Sized, Container, Reversible):
1066
    def __cinit__(self, TreeValue tv, TreeStorage storage):
1067
        self._st = storage
1068
        self._type = type(tv)
1069
        self._constraint = tv.constraint
1070
        self._child_constraints = {}
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081

    def __len__(self):
        return self._st.size()

    def __contains__(self, item):
        for v in self:
            if item == v:
                return True

        return False

1082 1083 1084 1085 1086 1087 1088 1089 1090
    cdef inline _SimplifiedConstraintProxy _transact(self, str key):
        cdef _SimplifiedConstraintProxy cons
        if key in self._child_constraints:
            return self._child_constraints[key]
        else:
            cons = _SimplifiedConstraintProxy(transact(self._constraint, key))
            self._child_constraints[key] = cons
            return cons

1091
    def _iter(self):
1092
        for k, v in self._st.iter_items():
1093
            if isinstance(v, TreeStorage):
1094
                yield self._type(v, self._transact(k))
1095 1096 1097 1098 1099 1100 1101
            else:
                yield v

    def __iter__(self):
        return self._iter()

    def _rev_iter(self):
1102
        for k, v in self._st.iter_rev_items():
1103
            if isinstance(v, TreeStorage):
1104
                yield self._type(v, self._transact(k))
1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
            else:
                yield v

    def __reversed__(self):
        if _reversible:
            return self._rev_iter()
        else:
            raise TypeError(f'{type(self).__name__!r} object is not reversible')

    def __repr__(self):
        return f'{type(self).__name__}({list(self)!r})'

# noinspection PyPep8Naming
cdef class treevalue_items(_CObject, Sized, Container, Reversible):
1119
    def __cinit__(self, TreeValue tv, TreeStorage storage):
1120
        self._st = storage
1121
        self._type = type(tv)
1122
        self._constraint = tv.constraint
1123
        self._child_constraints = {}
1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134

    def __len__(self):
        return self._st.size()

    def __contains__(self, item):
        for k, v in self:
            if item == (k, v):
                return True

        return False

1135 1136 1137 1138 1139 1140 1141 1142 1143
    cdef inline _SimplifiedConstraintProxy _transact(self, str key):
        cdef _SimplifiedConstraintProxy cons
        if key in self._child_constraints:
            return self._child_constraints[key]
        else:
            cons = _SimplifiedConstraintProxy(transact(self._constraint, key))
            self._child_constraints[key] = cons
            return cons

1144 1145 1146
    def _iter(self):
        for k, v in self._st.iter_items():
            if isinstance(v, TreeStorage):
1147
                yield k, self._type(v, self._transact(k))
1148 1149 1150 1151 1152 1153 1154 1155 1156
            else:
                yield k, v

    def __iter__(self):
        return self._iter()

    def _rev_iter(self):
        for k, v in self._st.iter_rev_items():
            if isinstance(v, TreeStorage):
1157
                yield k, self._type(v, self._transact(k))
1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169
            else:
                yield k, v

    def __reversed__(self):
        if _reversible:
            return self._rev_iter()
        else:
            raise TypeError(f'{type(self).__name__!r} object is not reversible')

    def __repr__(self):
        return f'{type(self).__name__}({list(self)!r})'

HansBug's avatar
HansBug 已提交
1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193
cdef class DetachedDelayedProxy(DelayedProxy):
    def __init__(self, DelayedProxy proxy):
        self.proxy = proxy
        self.calculated = False
        self.val = None

    cpdef object value(self):
        if not self.calculated:
            self.val = undelay(self.proxy, False)
            self.calculated = True

        return self.val

    cpdef object fvalue(self):
        cdef object v = self.value()
        if isinstance(v, TreeValue):
            v = v._detach()
        return v

@cython.binding(True)
def delayed(func, *args, **kwargs):
    r"""
    Overview:
        Use delayed function in treevalue.
HansBug's avatar
HansBug 已提交
1194 1195
        The given ``func`` will not be called until its value is accessed, and \
        it will be only called once, after that the delayed node will be replaced by the actual value.
HansBug's avatar
HansBug 已提交
1196

1197 1198 1199
    :param func: Delayed function.
    :param args: Positional arguments.
    :param kwargs: Key-word arguments.
HansBug's avatar
HansBug 已提交
1200

1201
    Examples:
HansBug's avatar
HansBug 已提交
1202 1203
        >>> from treevalue import TreeValue, delayed
        >>> def f(x):
1204 1205 1206
        ...     print('f is called, x is', x)
        ...     return x ** x
        ...
HansBug's avatar
HansBug 已提交
1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221
        >>> t = TreeValue({'a': delayed(f, 2), 'x': delayed(f, 3)})
        >>> t.a
        f is called, x is 2
        4
        >>> t.x
        f is called, x is 3
        27
        >>> t.a
        4
        >>> t.x
        27
        >>> t = TreeValue({'a': delayed(f, 2), 'x': delayed(f, 3)})
        >>> print(t)
        f is called, x is 2
        f is called, x is 3
1222 1223 1224
        <TreeValue 0x7f672fc53550>
        ├── 'a' --> 4
        └── 'x' --> 27
HansBug's avatar
HansBug 已提交
1225
        >>> print(t)
1226 1227 1228
        <TreeValue 0x7f672fc53550>
        ├── 'a' --> 4
        └── 'x' --> 27
HansBug's avatar
HansBug 已提交
1229 1230
    """
    return DetachedDelayedProxy(_c_delayed_partial(func, args, kwargs))