classicist 1.0.4__tar.gz → 1.0.5__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.
- {classicist-1.0.4/source/classicist.egg-info → classicist-1.0.5}/PKG-INFO +149 -6
- {classicist-1.0.4 → classicist-1.0.5}/README.md +148 -5
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/__init__.py +8 -0
- classicist-1.0.5/source/classicist/types/__init__.py +6 -0
- classicist-1.0.5/source/classicist/types/null/__init__.py +70 -0
- classicist-1.0.5/source/classicist/version.txt +1 -0
- {classicist-1.0.4 → classicist-1.0.5/source/classicist.egg-info}/PKG-INFO +149 -6
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist.egg-info/SOURCES.txt +3 -0
- classicist-1.0.5/tests/test_nulltype.py +265 -0
- classicist-1.0.4/source/classicist/version.txt +0 -1
- {classicist-1.0.4 → classicist-1.0.5}/LICENSE.md +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/pyproject.toml +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/requirements.development.txt +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/requirements.distribution.txt +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/requirements.txt +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/setup.cfg +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/aliased/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/annotation/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/classproperty/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/deprecated/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/hybridmethod/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/nocache/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/runtimer/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/exceptions/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/exceptions/decorators/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/exceptions/decorators/aliased/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/exceptions/decorators/annotation/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/exceptions/metaclasses/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/exceptions/metaclasses/shadowproof/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/inspector/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/logging/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/metaclasses/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/metaclasses/aliased/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist/metaclasses/shadowproof/__init__.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist.egg-info/dependency_links.txt +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist.egg-info/requires.txt +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist.egg-info/top_level.txt +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/source/classicist.egg-info/zip-safe +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/tests/test_aliased.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/tests/test_annotation.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/tests/test_classproperty.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/tests/test_deprecated.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/tests/test_hybridmethod.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/tests/test_runtimer.py +0 -0
- {classicist-1.0.4 → classicist-1.0.5}/tests/test_shadowproof.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: classicist
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Classy class decorators for Python.
|
|
5
5
|
Author: Daniel Sissman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -33,17 +33,20 @@ Dynamic: license-file
|
|
|
33
33
|
|
|
34
34
|
# Classicist: Classy Class Decorators & Extensions
|
|
35
35
|
|
|
36
|
-
The Classicist library provides several useful decorators
|
|
36
|
+
The Classicist library provides several useful decorators, functions, classes, and types
|
|
37
|
+
that offer useful behaviours and functionality, and help to fill some of the gaps in the
|
|
38
|
+
current standard library:
|
|
37
39
|
|
|
38
40
|
* `@hybridmethod` – a decorator that allows methods to be used both as class methods and as instance methods;
|
|
39
|
-
* `@classproperty` – a decorator that
|
|
41
|
+
* `@classproperty` – a decorator that allows class methods to be accessed as class properties;
|
|
40
42
|
* `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
|
|
41
|
-
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
|
|
43
|
+
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated, with support for adding optional arbitrary annotations;
|
|
42
44
|
* `@alias` – a decorator that can be used to add aliases to classes, methods defined within classes, module-level functions, and nested functions when overriding the aliasing scope;
|
|
43
45
|
* `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
|
|
44
|
-
* `@runtimer` – a decorator that can be used to time function and method calls;
|
|
46
|
+
* `@runtimer` – a decorator that can be used to gather call run time information for function and method calls;
|
|
45
47
|
* `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
|
|
46
|
-
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases
|
|
48
|
+
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases;
|
|
49
|
+
* `Null` – an alternative to `None`, useful when building custom data model classes and libraries, where supporting "null-safe" style access and navigation of the model's nested hierarchy is preferred.
|
|
47
50
|
|
|
48
51
|
The `classicist` library was previously named `hybridmethod` so if a prior version had
|
|
49
52
|
been installed, please update references to the new library name. Installation of the
|
|
@@ -554,6 +557,146 @@ except AttributeShadowingError as exception:
|
|
|
554
557
|
pass
|
|
555
558
|
```
|
|
556
559
|
|
|
560
|
+
#### NullType: Null-Safe Style Access for Data Models and Nested Class Hierarchies
|
|
561
|
+
|
|
562
|
+
The `NullType` class supports the creation of a `Null` singleton instance that offers
|
|
563
|
+
support for safely chaining nested attribute accesses without raising exceptions for
|
|
564
|
+
attributes that have no inherent value.
|
|
565
|
+
|
|
566
|
+
As Python currently lacks a null-aware navigation operator, such as `?.`, unlike many
|
|
567
|
+
other dynamic languages, for safely navigating nested object hierarchies which may
|
|
568
|
+
contain null attributes, the library offers the `NullType` and `Null` singleton as a
|
|
569
|
+
potential option to support this need in the interim. Consistent use of the `Null`
|
|
570
|
+
singleton in place of the standard `None` singleton, in relevant scenarios, such as
|
|
571
|
+
within a custom data model library, can allow for more expressive and clearer code that
|
|
572
|
+
does not require endless checks for intermediary or nested attribute existence.
|
|
573
|
+
|
|
574
|
+
However, there are some caveats to the use of the `NullType` and `Null` singleton as
|
|
575
|
+
these are not built-in features of the language, and Python does not offer support for
|
|
576
|
+
the creation of custom operators nor overriding the `is` operator for identity checking
|
|
577
|
+
which limits some of the use cases in which the `Null` singleton can be used.
|
|
578
|
+
|
|
579
|
+
With knowledge of these caveats and in the right scenarios, the `Null` singleton can
|
|
580
|
+
offer a good way to achieve clearer and more expressive code while navigating nested
|
|
581
|
+
object hierarchies without the clutter of nested attribute existence checks.
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
from __future__ import annotations
|
|
585
|
+
|
|
586
|
+
from classicist import Null
|
|
587
|
+
|
|
588
|
+
data: dict = {
|
|
589
|
+
"id": 1,
|
|
590
|
+
"name": "A",
|
|
591
|
+
"related": {
|
|
592
|
+
"id": 2,
|
|
593
|
+
"name": "B",
|
|
594
|
+
"related": {
|
|
595
|
+
"id": 3,
|
|
596
|
+
"name": "C",
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
class Model(object):
|
|
602
|
+
"""Sample Model class with some properties that reference nested Model instances."""
|
|
603
|
+
|
|
604
|
+
def __init__(self, data: dict):
|
|
605
|
+
if not isinstance(data, dict):
|
|
606
|
+
raise TypeError("The 'data' argument must reference a valid data dictionary!")
|
|
607
|
+
|
|
608
|
+
if not ("id" in data and isinstance(data["id"], int)):
|
|
609
|
+
raise ValueError("The 'data' must contain an 'id' key with an integer value!")
|
|
610
|
+
|
|
611
|
+
if not ("name" in data and isinstance(data["name"], str)):
|
|
612
|
+
raise ValueError("The 'data' must contain an 'name' key with an string value!")
|
|
613
|
+
|
|
614
|
+
self.data = data
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def id(self) -> int:
|
|
618
|
+
return self.data["id"]
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def name(self) -> str:
|
|
622
|
+
return self.data["name"]
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def related(self) -> Model | Null:
|
|
626
|
+
if data := self.data.get("related"):
|
|
627
|
+
return Model(data=data)
|
|
628
|
+
else:
|
|
629
|
+
return Null
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def relates(self) -> Model | Null:
|
|
633
|
+
if data := self.data.get("relates"):
|
|
634
|
+
return Model(data=data)
|
|
635
|
+
else:
|
|
636
|
+
return Null
|
|
637
|
+
|
|
638
|
+
# Create an instance of the sample Model data class
|
|
639
|
+
model = Model(data=data)
|
|
640
|
+
|
|
641
|
+
# Check that the expected data attributes are available
|
|
642
|
+
assert model.id == 1
|
|
643
|
+
assert model.name == "A"
|
|
644
|
+
|
|
645
|
+
# The model.related property references A/1 in the data above, so these properties exist
|
|
646
|
+
assert model.related
|
|
647
|
+
assert model.related.id
|
|
648
|
+
assert model.related.name
|
|
649
|
+
|
|
650
|
+
# Ensure the nested property values are as expected
|
|
651
|
+
assert model.related.id == 2
|
|
652
|
+
assert model.related.name == "B"
|
|
653
|
+
|
|
654
|
+
# Note that model.relates had no corresponding data, so the Model returns `Null` which
|
|
655
|
+
# still allows for nested attribute access, such as to `.id` and `.name` without raising
|
|
656
|
+
# any exceptions; the `Null` singleton also allows for `bool` comparison as shown below:
|
|
657
|
+
assert not model.relates
|
|
658
|
+
assert not model.relates.id
|
|
659
|
+
assert not model.relates.name
|
|
660
|
+
|
|
661
|
+
# There is no limit to the levels of nesting that `NullType` and the `Null` singleton
|
|
662
|
+
# can support, so long as a custom data model or library consistently returns `Null` for
|
|
663
|
+
# cases where the "null-safe" navigation is desired:
|
|
664
|
+
|
|
665
|
+
# model.related.related references 3/C in the data above, so these properties exist
|
|
666
|
+
assert model.related.related.id
|
|
667
|
+
assert model.related.related.name
|
|
668
|
+
|
|
669
|
+
# model.related.relates was not specified in the data above so the Model returns `Null`
|
|
670
|
+
assert not model.related.relates.id
|
|
671
|
+
assert not model.related.relates.name
|
|
672
|
+
|
|
673
|
+
# Ensure the nested property values are as expected
|
|
674
|
+
assert model.related.related.id == 3
|
|
675
|
+
assert model.related.related.name == "C"
|
|
676
|
+
|
|
677
|
+
# These features make it easy to write clearer more expressive code without boilerplate
|
|
678
|
+
# code to check for the availability of nested attributes or entities:
|
|
679
|
+
if isinstance(name := model.related.name, str):
|
|
680
|
+
print("model.related.name => %s" % (name))
|
|
681
|
+
|
|
682
|
+
# No exception is raised here even though model.relates is effectively "null":
|
|
683
|
+
if isinstance(name := model.relates.name, str):
|
|
684
|
+
print("model.relates.name => %s" % (name))
|
|
685
|
+
|
|
686
|
+
# However, there are some caveats as noted with `NullType` and the `Null` singleton as
|
|
687
|
+
# these are a third-party solution so we can only go so far in supplementing null-safe
|
|
688
|
+
# operator behaviour in the language; for example, we cannot perform identity checks to
|
|
689
|
+
# boolean values, True or False, or the actual None singleton value:
|
|
690
|
+
assert not model.relates is True # Notice the `assert not` as `assert` would fail here
|
|
691
|
+
assert not model.relates is False # Notice the `assert not` as `assert` would fail here
|
|
692
|
+
|
|
693
|
+
# Furthermore, we cannot use `None` identity comparison either:
|
|
694
|
+
assert not model.relates is None # Notice the `assert not` as `assert` would fail here
|
|
695
|
+
|
|
696
|
+
# We can however perform an identity check against the `Null` singleton if needed:
|
|
697
|
+
assert model.relates is Null
|
|
698
|
+
```
|
|
699
|
+
|
|
557
700
|
### Unit Tests
|
|
558
701
|
|
|
559
702
|
The Classicist library includes a suite of comprehensive unit tests which ensure that
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
# Classicist: Classy Class Decorators & Extensions
|
|
2
2
|
|
|
3
|
-
The Classicist library provides several useful decorators
|
|
3
|
+
The Classicist library provides several useful decorators, functions, classes, and types
|
|
4
|
+
that offer useful behaviours and functionality, and help to fill some of the gaps in the
|
|
5
|
+
current standard library:
|
|
4
6
|
|
|
5
7
|
* `@hybridmethod` – a decorator that allows methods to be used both as class methods and as instance methods;
|
|
6
|
-
* `@classproperty` – a decorator that
|
|
8
|
+
* `@classproperty` – a decorator that allows class methods to be accessed as class properties;
|
|
7
9
|
* `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
|
|
8
|
-
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
|
|
10
|
+
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated, with support for adding optional arbitrary annotations;
|
|
9
11
|
* `@alias` – a decorator that can be used to add aliases to classes, methods defined within classes, module-level functions, and nested functions when overriding the aliasing scope;
|
|
10
12
|
* `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
|
|
11
|
-
* `@runtimer` – a decorator that can be used to time function and method calls;
|
|
13
|
+
* `@runtimer` – a decorator that can be used to gather call run time information for function and method calls;
|
|
12
14
|
* `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
|
|
13
|
-
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases
|
|
15
|
+
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases;
|
|
16
|
+
* `Null` – an alternative to `None`, useful when building custom data model classes and libraries, where supporting "null-safe" style access and navigation of the model's nested hierarchy is preferred.
|
|
14
17
|
|
|
15
18
|
The `classicist` library was previously named `hybridmethod` so if a prior version had
|
|
16
19
|
been installed, please update references to the new library name. Installation of the
|
|
@@ -521,6 +524,146 @@ except AttributeShadowingError as exception:
|
|
|
521
524
|
pass
|
|
522
525
|
```
|
|
523
526
|
|
|
527
|
+
#### NullType: Null-Safe Style Access for Data Models and Nested Class Hierarchies
|
|
528
|
+
|
|
529
|
+
The `NullType` class supports the creation of a `Null` singleton instance that offers
|
|
530
|
+
support for safely chaining nested attribute accesses without raising exceptions for
|
|
531
|
+
attributes that have no inherent value.
|
|
532
|
+
|
|
533
|
+
As Python currently lacks a null-aware navigation operator, such as `?.`, unlike many
|
|
534
|
+
other dynamic languages, for safely navigating nested object hierarchies which may
|
|
535
|
+
contain null attributes, the library offers the `NullType` and `Null` singleton as a
|
|
536
|
+
potential option to support this need in the interim. Consistent use of the `Null`
|
|
537
|
+
singleton in place of the standard `None` singleton, in relevant scenarios, such as
|
|
538
|
+
within a custom data model library, can allow for more expressive and clearer code that
|
|
539
|
+
does not require endless checks for intermediary or nested attribute existence.
|
|
540
|
+
|
|
541
|
+
However, there are some caveats to the use of the `NullType` and `Null` singleton as
|
|
542
|
+
these are not built-in features of the language, and Python does not offer support for
|
|
543
|
+
the creation of custom operators nor overriding the `is` operator for identity checking
|
|
544
|
+
which limits some of the use cases in which the `Null` singleton can be used.
|
|
545
|
+
|
|
546
|
+
With knowledge of these caveats and in the right scenarios, the `Null` singleton can
|
|
547
|
+
offer a good way to achieve clearer and more expressive code while navigating nested
|
|
548
|
+
object hierarchies without the clutter of nested attribute existence checks.
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
from __future__ import annotations
|
|
552
|
+
|
|
553
|
+
from classicist import Null
|
|
554
|
+
|
|
555
|
+
data: dict = {
|
|
556
|
+
"id": 1,
|
|
557
|
+
"name": "A",
|
|
558
|
+
"related": {
|
|
559
|
+
"id": 2,
|
|
560
|
+
"name": "B",
|
|
561
|
+
"related": {
|
|
562
|
+
"id": 3,
|
|
563
|
+
"name": "C",
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
class Model(object):
|
|
569
|
+
"""Sample Model class with some properties that reference nested Model instances."""
|
|
570
|
+
|
|
571
|
+
def __init__(self, data: dict):
|
|
572
|
+
if not isinstance(data, dict):
|
|
573
|
+
raise TypeError("The 'data' argument must reference a valid data dictionary!")
|
|
574
|
+
|
|
575
|
+
if not ("id" in data and isinstance(data["id"], int)):
|
|
576
|
+
raise ValueError("The 'data' must contain an 'id' key with an integer value!")
|
|
577
|
+
|
|
578
|
+
if not ("name" in data and isinstance(data["name"], str)):
|
|
579
|
+
raise ValueError("The 'data' must contain an 'name' key with an string value!")
|
|
580
|
+
|
|
581
|
+
self.data = data
|
|
582
|
+
|
|
583
|
+
@property
|
|
584
|
+
def id(self) -> int:
|
|
585
|
+
return self.data["id"]
|
|
586
|
+
|
|
587
|
+
@property
|
|
588
|
+
def name(self) -> str:
|
|
589
|
+
return self.data["name"]
|
|
590
|
+
|
|
591
|
+
@property
|
|
592
|
+
def related(self) -> Model | Null:
|
|
593
|
+
if data := self.data.get("related"):
|
|
594
|
+
return Model(data=data)
|
|
595
|
+
else:
|
|
596
|
+
return Null
|
|
597
|
+
|
|
598
|
+
@property
|
|
599
|
+
def relates(self) -> Model | Null:
|
|
600
|
+
if data := self.data.get("relates"):
|
|
601
|
+
return Model(data=data)
|
|
602
|
+
else:
|
|
603
|
+
return Null
|
|
604
|
+
|
|
605
|
+
# Create an instance of the sample Model data class
|
|
606
|
+
model = Model(data=data)
|
|
607
|
+
|
|
608
|
+
# Check that the expected data attributes are available
|
|
609
|
+
assert model.id == 1
|
|
610
|
+
assert model.name == "A"
|
|
611
|
+
|
|
612
|
+
# The model.related property references A/1 in the data above, so these properties exist
|
|
613
|
+
assert model.related
|
|
614
|
+
assert model.related.id
|
|
615
|
+
assert model.related.name
|
|
616
|
+
|
|
617
|
+
# Ensure the nested property values are as expected
|
|
618
|
+
assert model.related.id == 2
|
|
619
|
+
assert model.related.name == "B"
|
|
620
|
+
|
|
621
|
+
# Note that model.relates had no corresponding data, so the Model returns `Null` which
|
|
622
|
+
# still allows for nested attribute access, such as to `.id` and `.name` without raising
|
|
623
|
+
# any exceptions; the `Null` singleton also allows for `bool` comparison as shown below:
|
|
624
|
+
assert not model.relates
|
|
625
|
+
assert not model.relates.id
|
|
626
|
+
assert not model.relates.name
|
|
627
|
+
|
|
628
|
+
# There is no limit to the levels of nesting that `NullType` and the `Null` singleton
|
|
629
|
+
# can support, so long as a custom data model or library consistently returns `Null` for
|
|
630
|
+
# cases where the "null-safe" navigation is desired:
|
|
631
|
+
|
|
632
|
+
# model.related.related references 3/C in the data above, so these properties exist
|
|
633
|
+
assert model.related.related.id
|
|
634
|
+
assert model.related.related.name
|
|
635
|
+
|
|
636
|
+
# model.related.relates was not specified in the data above so the Model returns `Null`
|
|
637
|
+
assert not model.related.relates.id
|
|
638
|
+
assert not model.related.relates.name
|
|
639
|
+
|
|
640
|
+
# Ensure the nested property values are as expected
|
|
641
|
+
assert model.related.related.id == 3
|
|
642
|
+
assert model.related.related.name == "C"
|
|
643
|
+
|
|
644
|
+
# These features make it easy to write clearer more expressive code without boilerplate
|
|
645
|
+
# code to check for the availability of nested attributes or entities:
|
|
646
|
+
if isinstance(name := model.related.name, str):
|
|
647
|
+
print("model.related.name => %s" % (name))
|
|
648
|
+
|
|
649
|
+
# No exception is raised here even though model.relates is effectively "null":
|
|
650
|
+
if isinstance(name := model.relates.name, str):
|
|
651
|
+
print("model.relates.name => %s" % (name))
|
|
652
|
+
|
|
653
|
+
# However, there are some caveats as noted with `NullType` and the `Null` singleton as
|
|
654
|
+
# these are a third-party solution so we can only go so far in supplementing null-safe
|
|
655
|
+
# operator behaviour in the language; for example, we cannot perform identity checks to
|
|
656
|
+
# boolean values, True or False, or the actual None singleton value:
|
|
657
|
+
assert not model.relates is True # Notice the `assert not` as `assert` would fail here
|
|
658
|
+
assert not model.relates is False # Notice the `assert not` as `assert` would fail here
|
|
659
|
+
|
|
660
|
+
# Furthermore, we cannot use `None` identity comparison either:
|
|
661
|
+
assert not model.relates is None # Notice the `assert not` as `assert` would fail here
|
|
662
|
+
|
|
663
|
+
# We can however perform an identity check against the `Null` singleton if needed:
|
|
664
|
+
assert model.relates is Null
|
|
665
|
+
```
|
|
666
|
+
|
|
524
667
|
### Unit Tests
|
|
525
668
|
|
|
526
669
|
The Classicist library includes a suite of comprehensive unit tests which ensure that
|
|
@@ -49,6 +49,11 @@ from classicist.exceptions import (
|
|
|
49
49
|
AttributeShadowingError,
|
|
50
50
|
)
|
|
51
51
|
|
|
52
|
+
from classicist.types import (
|
|
53
|
+
NullType,
|
|
54
|
+
Null,
|
|
55
|
+
)
|
|
56
|
+
|
|
52
57
|
__all__ = [
|
|
53
58
|
# Decorators
|
|
54
59
|
"alias",
|
|
@@ -75,4 +80,7 @@ __all__ = [
|
|
|
75
80
|
"AliasError",
|
|
76
81
|
"AnnotationError",
|
|
77
82
|
"AttributeShadowingError",
|
|
83
|
+
# Types
|
|
84
|
+
"NullType",
|
|
85
|
+
"Null",
|
|
78
86
|
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NullType(object):
|
|
5
|
+
"""The NullType class supports the creation of a Null singleton instance that offers
|
|
6
|
+
support for safely chaining nested attribute accesses without raising exceptions for
|
|
7
|
+
attributes that have no inherent value.
|
|
8
|
+
|
|
9
|
+
As Python currently lacks a null-aware navigation operator, such as `?.`, like many
|
|
10
|
+
other dynamic languages, for safely navigating nested object hierarchies which may
|
|
11
|
+
contain null attributes, the library offers the `NullType` and `Null` singleton as a
|
|
12
|
+
potential option to support this need in the interim. Consistent use of the `Null`
|
|
13
|
+
singleton in place of the standard `None` singleton, in relevant scenarios, such as
|
|
14
|
+
within a data model library for example, can allow for more expressive and clearer
|
|
15
|
+
code that does not require endless checks for intermediary attribute existence.
|
|
16
|
+
|
|
17
|
+
However, there are some caveats to the use of the `NullType` and `Null` singleton as
|
|
18
|
+
these are not built-in features of the language, and Python does not offer support
|
|
19
|
+
for the creation of custom operators nor overriding the `is` operator for identity
|
|
20
|
+
checking which limits some of the cases in which the `Null` singleton could be used.
|
|
21
|
+
|
|
22
|
+
With knowledge of the caveats and in the right scenarios, the `Null` singleton can
|
|
23
|
+
offer a good way to achieve clearer and more expressive code while navigating nested
|
|
24
|
+
object hierarchies without the clutter of nested attribute existence checks."""
|
|
25
|
+
|
|
26
|
+
_instance: NullType = None
|
|
27
|
+
|
|
28
|
+
def __new__(cls) -> NullType:
|
|
29
|
+
"""Ensure NullType can only create a singleton instance; further calls to create
|
|
30
|
+
instances of the NullType will return the existing singleton instance."""
|
|
31
|
+
|
|
32
|
+
if cls._instance is None:
|
|
33
|
+
cls._instance = super().__new__(cls)
|
|
34
|
+
|
|
35
|
+
return cls._instance
|
|
36
|
+
|
|
37
|
+
def __getattr__(self, name: str) -> NullType:
|
|
38
|
+
"""Support nested attribute access by returning the singleton instance."""
|
|
39
|
+
|
|
40
|
+
return self.__class__._instance
|
|
41
|
+
|
|
42
|
+
def __bool__(self) -> bool:
|
|
43
|
+
"""Support falsey equality checks for boolean comparisons against NullType."""
|
|
44
|
+
|
|
45
|
+
return False # Always returns False
|
|
46
|
+
|
|
47
|
+
def __eq__(self, value: object) -> bool:
|
|
48
|
+
"""Support value equality checks against NullType via the `==` operator as the
|
|
49
|
+
fallback option to being able to support identity equality checks via the `is`
|
|
50
|
+
operator which are not currently possible due to language constraints."""
|
|
51
|
+
|
|
52
|
+
if value is None or value is False:
|
|
53
|
+
return True
|
|
54
|
+
else:
|
|
55
|
+
return self is value
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return "Null"
|
|
59
|
+
|
|
60
|
+
def __repr__(self) -> str:
|
|
61
|
+
return "Null"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Create the singleton instance of Null
|
|
65
|
+
Null = NullType()
|
|
66
|
+
|
|
67
|
+
__all__ = [
|
|
68
|
+
"NullType",
|
|
69
|
+
"Null",
|
|
70
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.5
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: classicist
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Classy class decorators for Python.
|
|
5
5
|
Author: Daniel Sissman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -33,17 +33,20 @@ Dynamic: license-file
|
|
|
33
33
|
|
|
34
34
|
# Classicist: Classy Class Decorators & Extensions
|
|
35
35
|
|
|
36
|
-
The Classicist library provides several useful decorators
|
|
36
|
+
The Classicist library provides several useful decorators, functions, classes, and types
|
|
37
|
+
that offer useful behaviours and functionality, and help to fill some of the gaps in the
|
|
38
|
+
current standard library:
|
|
37
39
|
|
|
38
40
|
* `@hybridmethod` – a decorator that allows methods to be used both as class methods and as instance methods;
|
|
39
|
-
* `@classproperty` – a decorator that
|
|
41
|
+
* `@classproperty` – a decorator that allows class methods to be accessed as class properties;
|
|
40
42
|
* `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
|
|
41
|
-
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
|
|
43
|
+
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated, with support for adding optional arbitrary annotations;
|
|
42
44
|
* `@alias` – a decorator that can be used to add aliases to classes, methods defined within classes, module-level functions, and nested functions when overriding the aliasing scope;
|
|
43
45
|
* `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
|
|
44
|
-
* `@runtimer` – a decorator that can be used to time function and method calls;
|
|
46
|
+
* `@runtimer` – a decorator that can be used to gather call run time information for function and method calls;
|
|
45
47
|
* `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
|
|
46
|
-
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases
|
|
48
|
+
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases;
|
|
49
|
+
* `Null` – an alternative to `None`, useful when building custom data model classes and libraries, where supporting "null-safe" style access and navigation of the model's nested hierarchy is preferred.
|
|
47
50
|
|
|
48
51
|
The `classicist` library was previously named `hybridmethod` so if a prior version had
|
|
49
52
|
been installed, please update references to the new library name. Installation of the
|
|
@@ -554,6 +557,146 @@ except AttributeShadowingError as exception:
|
|
|
554
557
|
pass
|
|
555
558
|
```
|
|
556
559
|
|
|
560
|
+
#### NullType: Null-Safe Style Access for Data Models and Nested Class Hierarchies
|
|
561
|
+
|
|
562
|
+
The `NullType` class supports the creation of a `Null` singleton instance that offers
|
|
563
|
+
support for safely chaining nested attribute accesses without raising exceptions for
|
|
564
|
+
attributes that have no inherent value.
|
|
565
|
+
|
|
566
|
+
As Python currently lacks a null-aware navigation operator, such as `?.`, unlike many
|
|
567
|
+
other dynamic languages, for safely navigating nested object hierarchies which may
|
|
568
|
+
contain null attributes, the library offers the `NullType` and `Null` singleton as a
|
|
569
|
+
potential option to support this need in the interim. Consistent use of the `Null`
|
|
570
|
+
singleton in place of the standard `None` singleton, in relevant scenarios, such as
|
|
571
|
+
within a custom data model library, can allow for more expressive and clearer code that
|
|
572
|
+
does not require endless checks for intermediary or nested attribute existence.
|
|
573
|
+
|
|
574
|
+
However, there are some caveats to the use of the `NullType` and `Null` singleton as
|
|
575
|
+
these are not built-in features of the language, and Python does not offer support for
|
|
576
|
+
the creation of custom operators nor overriding the `is` operator for identity checking
|
|
577
|
+
which limits some of the use cases in which the `Null` singleton can be used.
|
|
578
|
+
|
|
579
|
+
With knowledge of these caveats and in the right scenarios, the `Null` singleton can
|
|
580
|
+
offer a good way to achieve clearer and more expressive code while navigating nested
|
|
581
|
+
object hierarchies without the clutter of nested attribute existence checks.
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
from __future__ import annotations
|
|
585
|
+
|
|
586
|
+
from classicist import Null
|
|
587
|
+
|
|
588
|
+
data: dict = {
|
|
589
|
+
"id": 1,
|
|
590
|
+
"name": "A",
|
|
591
|
+
"related": {
|
|
592
|
+
"id": 2,
|
|
593
|
+
"name": "B",
|
|
594
|
+
"related": {
|
|
595
|
+
"id": 3,
|
|
596
|
+
"name": "C",
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
class Model(object):
|
|
602
|
+
"""Sample Model class with some properties that reference nested Model instances."""
|
|
603
|
+
|
|
604
|
+
def __init__(self, data: dict):
|
|
605
|
+
if not isinstance(data, dict):
|
|
606
|
+
raise TypeError("The 'data' argument must reference a valid data dictionary!")
|
|
607
|
+
|
|
608
|
+
if not ("id" in data and isinstance(data["id"], int)):
|
|
609
|
+
raise ValueError("The 'data' must contain an 'id' key with an integer value!")
|
|
610
|
+
|
|
611
|
+
if not ("name" in data and isinstance(data["name"], str)):
|
|
612
|
+
raise ValueError("The 'data' must contain an 'name' key with an string value!")
|
|
613
|
+
|
|
614
|
+
self.data = data
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def id(self) -> int:
|
|
618
|
+
return self.data["id"]
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def name(self) -> str:
|
|
622
|
+
return self.data["name"]
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def related(self) -> Model | Null:
|
|
626
|
+
if data := self.data.get("related"):
|
|
627
|
+
return Model(data=data)
|
|
628
|
+
else:
|
|
629
|
+
return Null
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def relates(self) -> Model | Null:
|
|
633
|
+
if data := self.data.get("relates"):
|
|
634
|
+
return Model(data=data)
|
|
635
|
+
else:
|
|
636
|
+
return Null
|
|
637
|
+
|
|
638
|
+
# Create an instance of the sample Model data class
|
|
639
|
+
model = Model(data=data)
|
|
640
|
+
|
|
641
|
+
# Check that the expected data attributes are available
|
|
642
|
+
assert model.id == 1
|
|
643
|
+
assert model.name == "A"
|
|
644
|
+
|
|
645
|
+
# The model.related property references A/1 in the data above, so these properties exist
|
|
646
|
+
assert model.related
|
|
647
|
+
assert model.related.id
|
|
648
|
+
assert model.related.name
|
|
649
|
+
|
|
650
|
+
# Ensure the nested property values are as expected
|
|
651
|
+
assert model.related.id == 2
|
|
652
|
+
assert model.related.name == "B"
|
|
653
|
+
|
|
654
|
+
# Note that model.relates had no corresponding data, so the Model returns `Null` which
|
|
655
|
+
# still allows for nested attribute access, such as to `.id` and `.name` without raising
|
|
656
|
+
# any exceptions; the `Null` singleton also allows for `bool` comparison as shown below:
|
|
657
|
+
assert not model.relates
|
|
658
|
+
assert not model.relates.id
|
|
659
|
+
assert not model.relates.name
|
|
660
|
+
|
|
661
|
+
# There is no limit to the levels of nesting that `NullType` and the `Null` singleton
|
|
662
|
+
# can support, so long as a custom data model or library consistently returns `Null` for
|
|
663
|
+
# cases where the "null-safe" navigation is desired:
|
|
664
|
+
|
|
665
|
+
# model.related.related references 3/C in the data above, so these properties exist
|
|
666
|
+
assert model.related.related.id
|
|
667
|
+
assert model.related.related.name
|
|
668
|
+
|
|
669
|
+
# model.related.relates was not specified in the data above so the Model returns `Null`
|
|
670
|
+
assert not model.related.relates.id
|
|
671
|
+
assert not model.related.relates.name
|
|
672
|
+
|
|
673
|
+
# Ensure the nested property values are as expected
|
|
674
|
+
assert model.related.related.id == 3
|
|
675
|
+
assert model.related.related.name == "C"
|
|
676
|
+
|
|
677
|
+
# These features make it easy to write clearer more expressive code without boilerplate
|
|
678
|
+
# code to check for the availability of nested attributes or entities:
|
|
679
|
+
if isinstance(name := model.related.name, str):
|
|
680
|
+
print("model.related.name => %s" % (name))
|
|
681
|
+
|
|
682
|
+
# No exception is raised here even though model.relates is effectively "null":
|
|
683
|
+
if isinstance(name := model.relates.name, str):
|
|
684
|
+
print("model.relates.name => %s" % (name))
|
|
685
|
+
|
|
686
|
+
# However, there are some caveats as noted with `NullType` and the `Null` singleton as
|
|
687
|
+
# these are a third-party solution so we can only go so far in supplementing null-safe
|
|
688
|
+
# operator behaviour in the language; for example, we cannot perform identity checks to
|
|
689
|
+
# boolean values, True or False, or the actual None singleton value:
|
|
690
|
+
assert not model.relates is True # Notice the `assert not` as `assert` would fail here
|
|
691
|
+
assert not model.relates is False # Notice the `assert not` as `assert` would fail here
|
|
692
|
+
|
|
693
|
+
# Furthermore, we cannot use `None` identity comparison either:
|
|
694
|
+
assert not model.relates is None # Notice the `assert not` as `assert` would fail here
|
|
695
|
+
|
|
696
|
+
# We can however perform an identity check against the `Null` singleton if needed:
|
|
697
|
+
assert model.relates is Null
|
|
698
|
+
```
|
|
699
|
+
|
|
557
700
|
### Unit Tests
|
|
558
701
|
|
|
559
702
|
The Classicist library includes a suite of comprehensive unit tests which ensure that
|
|
@@ -31,10 +31,13 @@ source/classicist/logging/__init__.py
|
|
|
31
31
|
source/classicist/metaclasses/__init__.py
|
|
32
32
|
source/classicist/metaclasses/aliased/__init__.py
|
|
33
33
|
source/classicist/metaclasses/shadowproof/__init__.py
|
|
34
|
+
source/classicist/types/__init__.py
|
|
35
|
+
source/classicist/types/null/__init__.py
|
|
34
36
|
tests/test_aliased.py
|
|
35
37
|
tests/test_annotation.py
|
|
36
38
|
tests/test_classproperty.py
|
|
37
39
|
tests/test_deprecated.py
|
|
38
40
|
tests/test_hybridmethod.py
|
|
41
|
+
tests/test_nulltype.py
|
|
39
42
|
tests/test_runtimer.py
|
|
40
43
|
tests/test_shadowproof.py
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from classicist import NullType, Null
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_nulltype():
|
|
7
|
+
"""Test the `NullType` class."""
|
|
8
|
+
|
|
9
|
+
assert isinstance(NullType, type)
|
|
10
|
+
assert issubclass(NullType, object)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_nulltype_instantiation():
|
|
14
|
+
"""Test the instantiation of the `NullType` class."""
|
|
15
|
+
|
|
16
|
+
# Create an instance of the NullType class (this should return the singleton)
|
|
17
|
+
null1 = NullType()
|
|
18
|
+
|
|
19
|
+
# Ensure it has the expected type
|
|
20
|
+
assert isinstance(null1, NullType)
|
|
21
|
+
|
|
22
|
+
# Create an instance of the NullType class (this should return the singleton)
|
|
23
|
+
null2 = NullType()
|
|
24
|
+
|
|
25
|
+
# Ensure it has the expected type
|
|
26
|
+
assert isinstance(null2, NullType)
|
|
27
|
+
|
|
28
|
+
# Ensure that the NullType can only create and return a singleton instance; as all
|
|
29
|
+
# instances should pass identity checks with each other via the `is` operator:
|
|
30
|
+
assert null1 is null2
|
|
31
|
+
|
|
32
|
+
# Ensure any NullType instances are all the same, and are the same as the singleton
|
|
33
|
+
assert null1 is null2 is Null
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_nulltype_singleton():
|
|
37
|
+
"""Test the `Null` singleton instance's type."""
|
|
38
|
+
|
|
39
|
+
assert isinstance(Null, NullType)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_nulltype_string_representation():
|
|
43
|
+
"""Test the `Null` singleton instance's string representation."""
|
|
44
|
+
|
|
45
|
+
assert str(Null) == "Null"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_nulltype_debug_string_representation():
|
|
49
|
+
"""Test the `Null` singleton instance's debug string representation."""
|
|
50
|
+
|
|
51
|
+
assert repr(Null) == "Null"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_nulltype_arbitrary_attribute_access():
|
|
55
|
+
"""Test arbitrary attribute access on the `Null` singleton instance."""
|
|
56
|
+
|
|
57
|
+
thing = Null
|
|
58
|
+
|
|
59
|
+
assert thing.a is Null
|
|
60
|
+
assert thing.a.b is Null
|
|
61
|
+
assert thing.a.c is Null
|
|
62
|
+
assert thing.a.c.d is Null
|
|
63
|
+
assert thing.a.c.d.e is Null
|
|
64
|
+
assert thing.a.c.d.e.f is Null
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_nulltype_boolean_comparison():
|
|
68
|
+
"""Test arbitrary attribute boolean comparison on the `Null` singleton instance."""
|
|
69
|
+
|
|
70
|
+
thing: object = Null
|
|
71
|
+
|
|
72
|
+
# We expect that these non-existent nested attributes will boolean compare as falsey
|
|
73
|
+
# hence the use of `assert not` in the tests below:
|
|
74
|
+
assert not thing.a
|
|
75
|
+
assert not thing.a.b
|
|
76
|
+
assert not thing.a.c
|
|
77
|
+
assert not thing.a.c.d
|
|
78
|
+
assert not thing.a.c.d.e
|
|
79
|
+
assert not thing.a.c.d.e.f
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_nulltype_boolean_comparison_with_if_else_statement():
|
|
83
|
+
"""Test arbitrary attribute boolean comparison on the `Null` singleton instance."""
|
|
84
|
+
|
|
85
|
+
thing: object = Null
|
|
86
|
+
|
|
87
|
+
exists: bool = None
|
|
88
|
+
|
|
89
|
+
# Test the boolean comparison through an `if-else` statement
|
|
90
|
+
if thing.a.b.c.d.e.f:
|
|
91
|
+
exists = True
|
|
92
|
+
else:
|
|
93
|
+
exists = False # We expect to reach this assignment as `if` should not pass
|
|
94
|
+
|
|
95
|
+
# Ensure that the result of the if-else statement is as expected
|
|
96
|
+
assert exists is False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_nulltype_boolean_comparison_with_if_not_else_statement():
|
|
100
|
+
"""Test arbitrary attribute boolean comparison on the `Null` singleton instance."""
|
|
101
|
+
|
|
102
|
+
thing: object = Null
|
|
103
|
+
|
|
104
|
+
exists: bool = None
|
|
105
|
+
|
|
106
|
+
# Test the boolean comparison through an `if-else` statement
|
|
107
|
+
if not thing.a.b.c.d.e.f:
|
|
108
|
+
exists = False # We expect to reach this assignment as `if not` should pass
|
|
109
|
+
else:
|
|
110
|
+
exists = True
|
|
111
|
+
|
|
112
|
+
# Ensure that the result of the if-not-else statement is as expected
|
|
113
|
+
assert exists is False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_nulltype_identity_comparison():
|
|
117
|
+
"""Test arbitrary attribute boolean comparison on the `Null` singleton instance."""
|
|
118
|
+
|
|
119
|
+
thing: object = Null
|
|
120
|
+
|
|
121
|
+
# We expect that the identity comparisons to True should *not* pass
|
|
122
|
+
assert not thing.a is True
|
|
123
|
+
assert not thing.a.b is True
|
|
124
|
+
|
|
125
|
+
# We expect that the identity comparisons to False should *not* pass
|
|
126
|
+
assert not thing.a is False
|
|
127
|
+
assert not thing.a.b is False
|
|
128
|
+
|
|
129
|
+
# We expect that the identity comparisons to None should *not* pass
|
|
130
|
+
assert not thing.a is None
|
|
131
|
+
assert not thing.a.b is None
|
|
132
|
+
|
|
133
|
+
# We expect identity comparisons to anything other than `Null` should *not* pass
|
|
134
|
+
assert not thing.a == 123
|
|
135
|
+
assert not thing.a.b == 123
|
|
136
|
+
|
|
137
|
+
# We expect that the identity comparison to the `Null` singleton *will* pass
|
|
138
|
+
assert thing.a is Null
|
|
139
|
+
assert thing.a.b is Null
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_nulltype_with_a_custom_data_model():
|
|
143
|
+
"""Test the `Null` singleton instance in a custom data model to demonstrate the
|
|
144
|
+
ability to use the `Null` singleton instead of `None` for a null-safe experience."""
|
|
145
|
+
|
|
146
|
+
data: dict = {
|
|
147
|
+
"id": 1,
|
|
148
|
+
"name": "A",
|
|
149
|
+
"related": {
|
|
150
|
+
"id": 2,
|
|
151
|
+
"name": "B",
|
|
152
|
+
"related": {
|
|
153
|
+
"id": 3,
|
|
154
|
+
"name": "C",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
class Model(object):
|
|
160
|
+
"""Sample Model with some properties that reference nested Model instances."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, data: dict):
|
|
163
|
+
if not isinstance(data, dict):
|
|
164
|
+
raise TypeError(
|
|
165
|
+
"The 'data' argument must reference a valid data dictionary!"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if not ("id" in data and isinstance(data["id"], int)):
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"The 'data' must contain an 'id' key with an integer value!"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if not ("name" in data and isinstance(data["name"], str)):
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"The 'data' must contain an 'name' key with an string value!"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
self.data = data
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def id(self) -> int:
|
|
182
|
+
return self.data["id"]
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def name(self) -> str:
|
|
186
|
+
return self.data["name"]
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def related(self) -> Model | Null:
|
|
190
|
+
if data := self.data.get("related"):
|
|
191
|
+
return Model(data=data)
|
|
192
|
+
else:
|
|
193
|
+
return Null
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def relates(self) -> Model | Null:
|
|
197
|
+
if data := self.data.get("relates"):
|
|
198
|
+
return Model(data=data)
|
|
199
|
+
else:
|
|
200
|
+
return Null
|
|
201
|
+
|
|
202
|
+
# Create an instance of the sample Model data class
|
|
203
|
+
model = Model(data=data)
|
|
204
|
+
|
|
205
|
+
# Check that the expected data attributes are available
|
|
206
|
+
assert model.id == 1
|
|
207
|
+
assert model.name == "A"
|
|
208
|
+
|
|
209
|
+
# The model.related property references A/1 in the data above, so these properties exist
|
|
210
|
+
assert model.related
|
|
211
|
+
assert model.related.id
|
|
212
|
+
assert model.related.name
|
|
213
|
+
|
|
214
|
+
# Ensure the nested property values are as expected
|
|
215
|
+
assert model.related.id == 2
|
|
216
|
+
assert model.related.name == "B"
|
|
217
|
+
|
|
218
|
+
# Note that model.relates had no corresponding data, so the Model returns `Null` which
|
|
219
|
+
# still allows for nested attribute access, such as to `.id` and `.name` without raising
|
|
220
|
+
# any exceptions; the `Null` singleton also allows for `bool` comparison as shown below:
|
|
221
|
+
assert not model.relates
|
|
222
|
+
assert not model.relates.id
|
|
223
|
+
assert not model.relates.name
|
|
224
|
+
|
|
225
|
+
# There is no limit to the levels of nesting that `NullType` and the `Null` singleton
|
|
226
|
+
# can support, so long as a custom data model or library consistently returns `Null`
|
|
227
|
+
# for cases where the "null-safe" navigation is desired:
|
|
228
|
+
|
|
229
|
+
# model.related.related references 3/C in the data above, so these properties exist
|
|
230
|
+
assert model.related.related.id
|
|
231
|
+
assert model.related.related.name
|
|
232
|
+
|
|
233
|
+
# model.related.relates was not specified in the data above so the Model returns `Null`
|
|
234
|
+
assert not model.related.relates.id
|
|
235
|
+
assert not model.related.relates.name
|
|
236
|
+
|
|
237
|
+
# Ensure the nested property values are as expected
|
|
238
|
+
assert model.related.related.id == 3
|
|
239
|
+
assert model.related.related.name == "C"
|
|
240
|
+
|
|
241
|
+
# These features make it easy to write clearer more expressive code without
|
|
242
|
+
# boilerplate code to check for the availability of nested attributes or entities:
|
|
243
|
+
if isinstance(name := model.related.name, str):
|
|
244
|
+
print("model.related.name => %s" % (name))
|
|
245
|
+
|
|
246
|
+
# No exception is raised here even though model.relates is effectively "null":
|
|
247
|
+
if isinstance(name := model.relates.name, str):
|
|
248
|
+
print("model.relates.name => %s" % (name))
|
|
249
|
+
|
|
250
|
+
# However, there are some caveats as noted with `NullType` and the `Null` singleton
|
|
251
|
+
# as these are a third-party solution so we can only go so far in supplementing
|
|
252
|
+
# null-safe operator behaviour in the language; for example, we cannot perform
|
|
253
|
+
# identity checks to the boolean values, True or False, or None singleton value:
|
|
254
|
+
|
|
255
|
+
# Notice the `assert not` as `assert` would fail here
|
|
256
|
+
assert not model.relates is True
|
|
257
|
+
|
|
258
|
+
# Notice the `assert not` as `assert` would fail here
|
|
259
|
+
assert not model.relates is False
|
|
260
|
+
|
|
261
|
+
# Notice the `assert not` as `assert` would fail here
|
|
262
|
+
assert not model.relates is None
|
|
263
|
+
|
|
264
|
+
# We can however perform an identity check against the `Null` singleton if needed:
|
|
265
|
+
assert model.relates is Null
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.4
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classicist-1.0.4 → classicist-1.0.5}/source/classicist/decorators/classproperty/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classicist-1.0.4 → classicist-1.0.5}/source/classicist/exceptions/decorators/aliased/__init__.py
RENAMED
|
File without changes
|
{classicist-1.0.4 → classicist-1.0.5}/source/classicist/exceptions/decorators/annotation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|