Skip to content

Commit a9db1de

Browse files
committed
v1.0.1 - Unit tests, csv parsing functions, fix for empty()
- New parsing functions added to common.py - useful for parsing environmental variables - Added csv_parse for parsing simple one-line CSVs and trimming whitespace - Added csv_env for extracting a `list()` from a csv formatted environ var - Added keyval_parse for parsing key value pair CSVs i.e. `key:val,key:val` - Added keyval_env (same as csv_env, but for key value environ vars) - Now with unit tests in `tests.py` :) - Created unit tests for is_true, is_false and is_empty to ensure reliable bool checks - Created unit tests for csv_parse and keyval_parse to ensure reliable parsing - Added informational text to the module docblock for privex.helpers - Added string '0' for empty()'s zero check - Version bump to 1.0.1
1 parent 797b2dc commit a9db1de

4 files changed

Lines changed: 213 additions & 4 deletions

File tree

privex/helpers/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
"""
2+
Privex's Python Helpers - https://github.com/privex/python-helpers
3+
4+
X11 / MIT License
5+
6+
Submodules:
7+
8+
* common - Uncategorized functions and classes, including bool checks and data parsing
9+
* decorators - Class / function decorators
10+
* django - Django-specific functions/classes, only available if Django package is installed
11+
* net - Network related functions/classes such as ASN name lookup, and IP version bool checks
12+
13+
214
+===================================================+
315
| © 2019 Privex Inc. |
416
| https://www.privex.io |

privex/helpers/common.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
import argparse
4040
import logging
4141
import sys
42-
from typing import Sequence
42+
from os import getenv as env
43+
from typing import Sequence, List, Union, Tuple
4344

4445
log = logging.getLogger(__name__)
4546

@@ -97,14 +98,14 @@ def empty(v, zero: bool = False, itr: bool = False) -> bool:
9798
... print('Var x is None, blank string, or an empty dict/list/iterable')
9899
99100
:param v: The variable to check if it's empty
100-
:param zero: if zero=True, then return True if the variable is 0
101+
:param zero: if zero=True, then return True if the variable is int 0 or str '0'
101102
:param itr: if itr=True, then return True if the variable is ``[]``, ``{}``, or is an iterable and has 0 length
102103
:return bool is_blank: True if a variable is blank (``None``, ``''``, ``0``, ``[]`` etc.)
103104
:return bool is_blank: False if a variable has content (or couldn't be checked properly)
104105
"""
105106

106107
_check = [None, '']
107-
if zero: _check.append(0)
108+
if zero: _check += [0, '0']
108109
if v in _check: return True
109110
if itr:
110111
if v == [] or v == {}: return True
@@ -163,6 +164,67 @@ def is_false(v, chk_none: bool = True) -> bool:
163164
chk += [None, 'none', 'null', ''] if chk_none else []
164165
return v in chk
165166

167+
def keyval_parse(line: str) -> List[Tuple[str, str]]:
168+
"""
169+
Parses a csv with key:value pairs such as:
170+
171+
John Alex:Doe,Jane Sarah:Doe
172+
173+
Into a list with tuple pairs (can be easily converted to a dict):
174+
175+
[
176+
('John Alex', 'Doe'),
177+
('Jane Sarah', 'Doe')
178+
]
179+
180+
:param str line: A string of key:value pairs separated by commas e.g. "John Alex:Doe,Jane Sarah:Doe"
181+
:return List[Tuple[str,str]] parsed_data: A list of (key, value) tuples that can easily be casted to a dict()
182+
"""
183+
line = [tuple(a.split(':')) for a in line.split(',')] if line != '' else []
184+
return [(a.strip(), b.strip()) for a, b in line]
185+
186+
def keyval_env(env_key: str, env_default = None) -> List[Tuple[str, str]]:
187+
"""
188+
Parses "key:val,key:val" into a list of tuples [(key,val), (key,val)]
189+
190+
See :py:meth:`keyval_parse`
191+
"""
192+
d = env(env_key)
193+
return env_default if empty(d) else keyval_parse(d)
194+
195+
def csv_parse(line: str) -> List[str]:
196+
"""
197+
Quick n' dirty parsing of a simple comma separated line, with automatic whitespace stripping
198+
of both the ``line`` itself, and the values within the commas.
199+
200+
Example:
201+
202+
>>> csv_parse(' hello , world, test')
203+
['hello', 'world', 'test']
204+
205+
"""
206+
return [x.strip() for x in line.strip().split(',')]
207+
208+
def csv_env(env_key: str, env_default = None) -> List[str]:
209+
"""
210+
Quick n' dirty parsing of simple CSV formatted environment variables, with fallback
211+
to user specified ``env_default`` (defaults to None)
212+
213+
Example:
214+
215+
>>> os.setenv('EXAMPLE', ' hello , world, test')
216+
>>> csv_env('EXAMPLE', [])
217+
['hello', 'world', 'test']
218+
>>> csv_env('NONEXISTANT', [])
219+
[]
220+
221+
:param str env_key: Environment var to attempt to load
222+
:param any env_default: Fallback value if the env var is empty / not set
223+
:return List[str] parsed_data: A list of str values parsed from the env var
224+
"""
225+
d = env(env_key)
226+
return env_default if empty(d) else csv_parse(d)
227+
166228

167229
class ErrHelpParser(argparse.ArgumentParser):
168230
"""

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
setup(
4343
name='privex_helpers',
4444

45-
version='1.0.0',
45+
version='1.0.1',
4646

4747
description='A variety of helper functions and classes, useful for many different projects',
4848
long_description=long_description,

tests.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
#!/usr/bin/env python3.7
2+
import unittest
3+
import logging
4+
from collections import namedtuple
5+
from privex import helpers
6+
from privex.loghelper import LogHelper
7+
8+
9+
class TestParseHelpers(unittest.TestCase):
10+
"""Test the parsing functions csv_parse and keyval_parse"""
11+
12+
def test_csv_spaced(self):
13+
"""Test csv parsing with excess outer whitespace, and value whitespace"""
14+
c = helpers.csv_parse(' valid , spaced out, csv ')
15+
self.assertListEqual(c, ['valid', 'spaced out', 'csv'])
16+
17+
def test_csv_single(self):
18+
"""Test that a single value still returns a list"""
19+
self.assertListEqual(helpers.csv_parse('single'), ['single'])
20+
21+
def test_kval_clean(self):
22+
"""Test that a clean key:val csv is parsed correctly"""
23+
self.assertListEqual(
24+
helpers.keyval_parse('John:Doe,Jane:Smith'),
25+
[('John', 'Doe'), ('Jane', 'Smith')]
26+
)
27+
28+
def test_kval_spaced(self):
29+
"""Test key:val csv parsing with excess outer whitespace, and value whitespace"""
30+
self.assertListEqual(
31+
helpers.keyval_parse(' John : Doe , Jane : Smith '),
32+
[('John', 'Doe'), ('Jane', 'Smith')]
33+
)
34+
35+
def test_kval_single(self):
36+
"""Test that a single value still returns a list"""
37+
self.assertListEqual(
38+
helpers.keyval_parse('John:Doe'),
39+
[('John', 'Doe')]
40+
)
41+
42+
class EmptyIter(object):
43+
"""A mock iterable object with zero length for testing empty()"""
44+
def __len__(self):
45+
return 0
46+
47+
class TestBoolHelpers(unittest.TestCase):
48+
"""Test the boolean check functions is_true, is_false, as well as empty()"""
49+
50+
falsey = ['false', 'FALSE', None, False, '', 0, '0', 'no', 'null']
51+
truthy = [True, 'TRUE', 'true', 'yes', 'y', '1', 1]
52+
empty_vals = [None, '']
53+
empty_lst = empty_vals + [[], (), set(), {}, EmptyIter()]
54+
empty_zero = empty_vals + [0, '0']
55+
56+
def test_isfalse_falsey(self):
57+
for f in self.falsey:
58+
self.assertTrue(helpers.is_false(f), msg=f"is_false({repr(f)}")
59+
60+
def test_isfalse_truthy(self):
61+
for f in self.truthy:
62+
self.assertFalse(helpers.is_false(f), msg=f"!is_false({repr(f)}")
63+
64+
def test_istrue_truthy(self):
65+
for f in self.truthy:
66+
self.assertTrue(helpers.is_true(f), msg=f"is_true({repr(f)}")
67+
68+
def test_istrue_falsey(self):
69+
for f in self.falsey:
70+
self.assertFalse(helpers.is_true(f), msg=f"!is_true({repr(f)}")
71+
72+
def test_empty_vals(self):
73+
for f in self.empty_vals:
74+
self.assertTrue(helpers.empty(f), msg=f"empty({repr(f)})")
75+
76+
def test_empty_lst(self):
77+
for f in self.empty_lst:
78+
self.assertTrue(helpers.empty(f, itr=True), msg=f"empty({repr(f)})")
79+
80+
def test_empty_zero(self):
81+
for f in self.empty_zero:
82+
self.assertTrue(helpers.empty(f, zero=True), msg=f"empty({repr(f)})")
83+
84+
def test_empty_combined(self):
85+
for f in self.empty_zero + self.empty_lst:
86+
self.assertTrue(helpers.empty(f, zero=True, itr=True), msg=f"empty({repr(f)})")
87+
88+
def test_notempty(self):
89+
# Basic string test
90+
self.assertFalse(helpers.empty('hello'))
91+
# Integer test
92+
self.assertFalse(helpers.empty(1, zero=True))
93+
# Iterable tests
94+
self.assertFalse(helpers.empty(['world'], itr=True))
95+
self.assertFalse(helpers.empty(('world',), itr=True))
96+
self.assertFalse(helpers.empty({'hello': 'world'}, itr=True))
97+
98+
99+
if __name__ == '__main__':
100+
unittest.main()
101+
102+
"""
103+
+===================================================+
104+
| © 2019 Privex Inc. |
105+
| https://www.privex.io |
106+
+===================================================+
107+
| |
108+
| Originally Developed by Privex Inc. |
109+
| |
110+
| Core Developer(s): |
111+
| |
112+
| (+) Chris (@someguy123) [Privex] |
113+
| (+) Kale (@kryogenic) [Privex] |
114+
| |
115+
+===================================================+
116+
117+
Copyright 2019 Privex Inc. ( https://www.privex.io )
118+
119+
Permission is hereby granted, free of charge, to any person obtaining a copy of
120+
this software and associated documentation files (the "Software"), to deal in
121+
the Software without restriction, including without limitation the rights to use,
122+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
123+
Software, and to permit persons to whom the Software is furnished to do so,
124+
subject to the following conditions:
125+
126+
The above copyright notice and this permission notice shall be included in all
127+
copies or substantial portions of the Software.
128+
129+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
130+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
131+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
132+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
133+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
134+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
135+
"""

0 commit comments

Comments
 (0)