|
1 | | -# nullsafe-python |
2 | | -Null safe python in real life ! |
| 1 | +# Null Safe Python |
| 2 | + |
| 3 | +Null safe support for Python. |
| 4 | + |
| 5 | +## Installation |
| 6 | + |
| 7 | +```bash |
| 8 | +pip install nullsafe |
| 9 | +``` |
| 10 | + |
| 11 | +## Quick Start |
| 12 | + |
| 13 | +Normal Python code |
| 14 | + |
| 15 | +```python |
| 16 | +o = object() |
| 17 | + |
| 18 | +try: |
| 19 | + value = o.inexistent |
| 20 | + print("accessed") |
| 21 | +except AttributeError: |
| 22 | + value = None |
| 23 | +``` |
| 24 | + |
| 25 | +With nullsafe: |
| 26 | + |
| 27 | +```python |
| 28 | +from nullsafe import undefined, _ |
| 29 | + |
| 30 | +o = object() |
| 31 | + |
| 32 | +value = _(o).inexistent |
| 33 | + |
| 34 | +if value is not undefined: |
| 35 | + print("accessed") |
| 36 | +``` |
| 37 | + |
| 38 | +## Documentation |
| 39 | + |
| 40 | +### Basics |
| 41 | + |
| 42 | +There are 5 values importable in nullsafe root: |
| 43 | + |
| 44 | +#### class `NullSafeProxy: (o: T)` |
| 45 | + |
| 46 | +Receives an object `o` on instantiation. |
| 47 | + |
| 48 | +Proxy class for granting nullsafe abilities to an object. |
| 49 | + |
| 50 | +#### class `NullSafe: ()` |
| 51 | + |
| 52 | +No argument needed. |
| 53 | + |
| 54 | +Nullish class with with nullsafe abilities. Instances will have a falsy boolean evaluation, equity comparison (`==`) to `None` and instance of `NullSafe` returns `True`, otherwise `False`. Identity comparison (`is`) to `None` will return `False`. |
| 55 | + |
| 56 | +#### variable `undefined: NullSafe` |
| 57 | + |
| 58 | +Instance of `Nullsafe`, this instance will be returned for all nullish access in a proxied object, enabling identity comparison `value is undefined` for code clarity. |
| 59 | + |
| 60 | +#### function `nullsafe: (o: T) -> T` |
| 61 | + |
| 62 | +Receives an object `o` as argument. |
| 63 | + |
| 64 | +Helper function that checks if object is nullish and return the proxied object. |
| 65 | + |
| 66 | +return `undefined` if `o` is `None` or `undefined`, otherwise return the proxied object `NullSafeProxy[T]`. |
| 67 | + |
| 68 | +This function is **generic typed** (`(o: T) -> T`), code autocompletions and linters functionalities will remain. Disclaimer: If the object was not typed before proxy, it obviously won't come out typed out of the blue. |
| 69 | + |
| 70 | +#### function `_: (o: T) -> T` (alias to `nullsafe`) |
| 71 | + |
| 72 | +Alias to nullsafe, used for better code clarity. |
| 73 | + |
| 74 | +The examples shown will be using `_` instead of `nullsafe` for code clarity. For better understanding, the Javascript equivalents will be shown as comments. |
| 75 | + |
| 76 | +### Implementation |
| 77 | + |
| 78 | +Nullsafe abilities are granted after proxying an object through `NullSafeProxy`. To proxy an object pass it through `_()` or `nullsafe()`. Due to language limitation, the implementation does not follow the "return the first nullish value in chain", instead it "extend the a custom nullish value until the end of chain". Inexistent values of a proxied object and its subsequent values in chain will return `undefined`. |
| 79 | + |
| 80 | +### Import |
| 81 | + |
| 82 | +```python |
| 83 | +from nullsafe import undefined, _ |
| 84 | +``` |
| 85 | + |
| 86 | +### Usage |
| 87 | + |
| 88 | +There are various way to get a nullsafe proxied object. |
| 89 | + |
| 90 | +#### Null safe attribute access |
| 91 | + |
| 92 | +Proxied object doing a possibly `AttributeError` access. |
| 93 | + |
| 94 | +```python |
| 95 | +o = SomeClass() |
| 96 | + |
| 97 | +# o.inexistent |
| 98 | +assert _(o).inexistent is undefined |
| 99 | +assert _(o).inexistent == None # Same to all below |
| 100 | +assert not _(o).inexistent # Same to all below |
| 101 | + |
| 102 | +# o.inexistent?.nested |
| 103 | +assert _(o).inexistent.nested is undefined |
| 104 | + |
| 105 | +# o.existent.inexistent?.nested |
| 106 | +assert _(o.existent).inexistent.nested is undefined |
| 107 | + |
| 108 | +# o.maybe?.inexistent?.nested |
| 109 | +assert _(_(o).maybe).inexistent.nested is undefined |
| 110 | +``` |
| 111 | + |
| 112 | +### Null safe item access |
| 113 | + |
| 114 | +Proxied object doing a possibly `KeyError` access. |
| 115 | + |
| 116 | +```python |
| 117 | +o = SomeClass() # dict works too ! |
| 118 | + |
| 119 | +# o.inexistent |
| 120 | +assert _(o)["inexistent"] is undefined |
| 121 | +assert _(o)["inexistent"] == None # Same to all below |
| 122 | +assert not _(o)["inexistent"] # Same to all below |
| 123 | + |
| 124 | +# o.inexistent?.nested |
| 125 | +assert _(o)["inexistent"]["nested"] is undefined |
| 126 | + |
| 127 | +# o.existent.inexistent?.nested |
| 128 | +assert _(o["existent"])["inexistent"]["nested"] is undefined |
| 129 | + |
| 130 | +# o.maybe?.inexistent?.nested |
| 131 | +assert _(_(o)["maybe"])["inexistent"]["nested"] is undefined |
| 132 | +``` |
| 133 | + |
| 134 | +#### Null safe post evaluation |
| 135 | + |
| 136 | +Possibly `None` or `undefined` object doing possibly `AttributeError` or `KeyError` access. |
| 137 | + |
| 138 | +Note: This only works if the seeking value is accessible, see [limitations](#post-evaluation) |
| 139 | + |
| 140 | +```python |
| 141 | +o = SomeClass() # dict works too ! |
| 142 | +o.nay = None |
| 143 | + |
| 144 | +# o.nay?.inexistent |
| 145 | +assert _(o.nay).inexistent is undefined |
| 146 | +assert _(o.nay).inexistent == None # Same to all below |
| 147 | +assert not _(o.nay).inexistent # Same to all below |
| 148 | + |
| 149 | +# o.nay?.inexistent.nested |
| 150 | +assert _(o.nay).inexistent.nested is undefined |
| 151 | +``` |
| 152 | + |
| 153 | +```python |
| 154 | +o = SomeClass() # dict works too ! |
| 155 | +o["nay"] = None |
| 156 | + |
| 157 | +# o.nay?.inexistent |
| 158 | +assert _(o["nay"])["inexistent"] is undefined |
| 159 | +assert not _(o["nay"])["inexistent"] |
| 160 | + |
| 161 | +# o.nay?.inexistent.nested |
| 162 | +assert _(o["nay"])["inexistent"]["nested"] is undefined |
| 163 | +assert not _(o["nay"])["inexistent"]["nested"] |
| 164 | +``` |
| 165 | + |
| 166 | +#### Combined usage |
| 167 | + |
| 168 | +Of course you can combine different styles. |
| 169 | + |
| 170 | +```python |
| 171 | +assert _(o).inexistent["inexistent"].inexistent.inexistent["inexistent"]["inexistent"] is undefined |
| 172 | +``` |
| 173 | + |
| 174 | +### Limitations |
| 175 | + |
| 176 | +List of limitations that you may encounter. |
| 177 | + |
| 178 | +#### `undefined` behavior |
| 179 | + |
| 180 | +`undefined` is actually an instance of `NullSafe`, the actual mechanism used for nullsafe chaining, it cannot self rip the nullsafe functionality when the chain ends (because it doesn't know), so this following actually possible and probably not the wanted behavior. |
| 181 | + |
| 182 | +```python |
| 183 | +val = _(o).inexistent |
| 184 | + |
| 185 | +assert val.another_inexistent is undefined |
| 186 | +``` |
| 187 | + |
| 188 | +#### Post evaluation |
| 189 | + |
| 190 | +In other languages like Javascript, it checks for each item in the chain and return `undefined` on the first nullish value, which in fact is post-evaluated. This is not possible in python because it raises an `AttributeError` or `KeyError` on access attempt, unless it returns `None` (see [one of the available usage](#null-safe-post-evaluation)), so it must proxy the instance that may contain the attr or key before accessing. |
| 191 | + |
| 192 | +```python |
| 193 | +try: |
| 194 | + val = _(o.inexistent).nested # AttributeError: '<type>' object has no attribute 'inexistent' |
| 195 | +except AttributeError: |
| 196 | + assert True |
| 197 | +assert _(o).inexistent.nested is undefined |
| 198 | +``` |
| 199 | + |
| 200 | +## Contributing |
| 201 | + |
| 202 | +Contributions welcomed ! Make sure it passes current tests tho. |
| 203 | + |
0 commit comments