Skip to content

Commit 871f06f

Browse files
committed
New: Release 0.1.0
1 parent a79f24b commit 871f06f

7 files changed

Lines changed: 538 additions & 2 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,8 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
development.ipynb
131+
132+
# vscode shits
133+
134+
.vscode/

README.md

Lines changed: 203 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,203 @@
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+

nullsafe/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .core import nullsafe, undefined, NullSafeProxy, NullSafe
2+
3+
_ = nullsafe

nullsafe/core.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from typing import Any, Generic, TypeVar
2+
from functools import wraps
3+
4+
5+
T = TypeVar("T")
6+
7+
8+
class NullSafe:
9+
10+
def __getattr__(self, k: str):
11+
return undefined
12+
13+
def __getitem__(self, k: str):
14+
return undefined
15+
16+
def __bool__(self):
17+
return False
18+
19+
def __eq__(self, o: object) -> bool:
20+
if o is None or o is undefined or isinstance(o, NullSafe):
21+
return True
22+
return False
23+
24+
def __repr__(self) -> str:
25+
return "undefined"
26+
27+
def __str__(self) -> str:
28+
return "undefined"
29+
30+
def __setattr__(self, name: str, value: Any) -> None:
31+
raise AttributeError(f"'{self.__class__.__name__}' object can't set attribute")
32+
33+
34+
undefined = NullSafe()
35+
36+
37+
class NullSafeProxy(Generic[T]):
38+
39+
__o: T
40+
41+
def __init__(self, o: T) -> None:
42+
self.__o = o
43+
44+
def __getitem__(self, k: str) -> Any:
45+
try:
46+
val = self.__o.__getitem__(k)
47+
if val is None:
48+
return undefined
49+
return val
50+
except (KeyError, AttributeError):
51+
return undefined
52+
53+
def __getattr__(self, name: str) -> Any:
54+
try:
55+
val = getattr(self.__o, name)
56+
if val is None:
57+
return undefined
58+
return val
59+
except AttributeError:
60+
return undefined
61+
62+
def __setattr__(self, name: str, value: Any) -> None:
63+
if name == "_NullSafeProxy__o":
64+
return super().__setattr__(name, value)
65+
raise AttributeError(f"'{self.__class__.__name__}' object can't set attribute")
66+
67+
68+
def nullsafe(o: T) -> T:
69+
if o == None:
70+
return undefined
71+
return NullSafeProxy(o)

setup.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python
2+
3+
import sys
4+
from os import path
5+
6+
from setuptools import setup, find_packages
7+
8+
9+
this_directory = path.abspath(path.dirname(__file__))
10+
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
11+
long_description = f.read()
12+
13+
14+
install_requires = []
15+
16+
17+
# Require python 3.7
18+
if sys.version_info.major != 3 and sys.version_info.minor < 7:
19+
sys.exit("'nullsafe' requires Python >= 3.7!")
20+
21+
22+
setup(
23+
name="nullsafe",
24+
version="0.1.0",
25+
author="Paaksing",
26+
author_email="paaksingtech@gmail.com",
27+
url="https://github.com/paaksing/nullsafe-python",
28+
description="Null safe support for Python",
29+
long_description=long_description,
30+
long_description_content_type='text/markdown',
31+
keywords=["null-safe", "nullsafe", "none aware", "python"],
32+
classifiers=[
33+
"Development Status :: 5 - Production/Stable",
34+
"Programming Language :: Python :: 3.7",
35+
"Environment :: Plugins",
36+
"Intended Audience :: Developers",
37+
"License :: OSI Approved :: MIT License",
38+
"Natural Language :: English",
39+
"Operating System :: OS Independent",
40+
"Topic :: Software Development :: Libraries :: Python Modules",
41+
"Topic :: Utilities",
42+
"Typing :: Typed",
43+
],
44+
license="MIT",
45+
packages=find_packages(exclude=("test")),
46+
zip_safe=True,
47+
install_requires=install_requires,
48+
include_package_data=True,
49+
)

test/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)