Skip to content

Commit 12da829

Browse files
committed
Add local_tests.sh, fix get_function_params on older python
- Added `local_tests.sh` for running the unit tests on multiple python versions locally - Added `OrderedDictObject` to collections module, since python versions before 3.8 cannot reverse a normal dict. - Add unit tests for ordered dict object - Adjusted `get_function_params` to use the new OrderedDictObject (fixes failing tests on older python versions)
1 parent 2fd2f1c commit 12da829

File tree

4 files changed

+262
-6
lines changed

4 files changed

+262
-6
lines changed

local_tests.sh

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/usr/bin/env bash
2+
################################################################
3+
# #
4+
# Local development test runner script for: #
5+
# #
6+
# Privex Python Helpers #
7+
# (C) 2019 Privex Inc. GNU AGPL v3 #
8+
# #
9+
# Privex Site: https://www.privex.io/ #
10+
# #
11+
# Github Repo: https://github.com/Privex/python-helpers #
12+
# #
13+
################################################################
14+
#
15+
# Basic Usage:
16+
#
17+
# ./local_tests.sh
18+
#
19+
# Run only specific tests (and don't update deps):
20+
#
21+
# ./local_deps tests/test_general.py tests/test_collections.py
22+
#
23+
################################################################
24+
#
25+
# Runs the unit tests across multiple Python versions locally, similar to Travis-CI.
26+
#
27+
# If pyenv is available, will install all python versions listed in PYENV_VERS into pyenv, and
28+
# create a virtualenv for each version.
29+
#
30+
# If pyenv is unavailable, will attempt to use the system python executables listed in PY_VERS
31+
# (will skip any that aren't available).
32+
#
33+
# To force use of system python EXE's, set env var USE_PYENV=0 like so:
34+
#
35+
# USE_PYENV=0 ./local_tests.sh
36+
#
37+
################################################################
38+
39+
set -e
40+
41+
# Error handling function for ShellCore
42+
_sc_fail() { echo >&2 "Failed to load or install Privex ShellCore..." && exit 1; }
43+
# If `load.sh` isn't found in the user install / global install, then download and run the auto-installer
44+
# from Privex's CDN.
45+
[[ -f "${HOME}/.pv-shcore/load.sh" ]] || [[ -f "/usr/local/share/pv-shcore/load.sh" ]] ||
46+
{ curl -fsS https://cdn.privex.io/github/shell-core/install.sh | bash >/dev/null; } || _sc_fail
47+
48+
# Attempt to load the local install of ShellCore first, then fallback to global install if it's not found.
49+
[[ -d "${HOME}/.pv-shcore" ]] && source "${HOME}/.pv-shcore/load.sh" ||
50+
source "/usr/local/share/pv-shcore/load.sh" || _sc_fail
51+
52+
autoupdate_shellcore
53+
54+
sg_load_lib trap
55+
56+
: ${USE_PYENV=1}
57+
58+
if [ -z ${PY_VERS+x} ]; then
59+
PY_VERS=("python3.6" "python3.7" "python3.8")
60+
fi
61+
62+
if [ -z ${PYENV_VERS+x} ]; then
63+
PYENV_VERS=("3.6.7" "3.7.1" "3.8.0")
64+
fi
65+
66+
###
67+
# Python Virtualenv shortcuts
68+
###
69+
70+
activate() {
71+
local envdir="./venv"
72+
if [[ "$#" -gt 0 ]]; then envdir="$1"; fi
73+
source "${envdir}/bin/activate"
74+
msg bold green "Activated virtualenv in $envdir"
75+
}
76+
77+
# Usage: mkvenv [python_exe] [env_folder]
78+
# mkvenv # no args = use system python3 and make in ./venv
79+
# mkvenv python3.7 # use system python3.7 and make in ./venv
80+
# mkvenv python3.6 ./env # use system python3.6 and make in ./env
81+
mkvenv() {
82+
local pyexe="python3"
83+
local envdir="./venv"
84+
if [[ "$#" -gt 0 ]]; then pyexe="$1"; fi
85+
if [[ "$#" -gt 1 ]]; then envdir="$2"; fi
86+
local pyver=$(/usr/bin/env "$pyexe" -V)
87+
/usr/bin/env "$pyexe" -m venv "$envdir"
88+
msg bold green "Made virtual env using $pyver @ $envdir"
89+
}
90+
91+
pyenv_install() {
92+
(($# < 1)) && msg bold red "ERROR: pyenv_install expects at least 1 arg - python version to install" && return 1
93+
94+
local os_name="$(uname -s)" py_ver="$1"
95+
if [[ "$os_name" == "Darwin" ]]; then
96+
export CFLAGS="-I$(brew --prefix readline)/include -I$(brew --prefix openssl)/include"
97+
export CFLAGS="${CFLAGS} -I$(xcrun --show-sdk-path)/usr/include"
98+
export LDFLAGS="-L$(brew --prefix readline)/lib -L$(brew --prefix openssl)/lib"
99+
export PYTHON_CONFIGURE_OPTS="--enable-unicode=ucs2"
100+
fi
101+
msg bold green " >>> Installing Python ${py_ver} via pyenv..."
102+
pyenv install -v "$py_ver"
103+
msg bold green " >>> Successfully installed Python ${py_ver}"
104+
}
105+
106+
main_tests() {
107+
if [[ -d "$VENV_PY_VER" ]]; then
108+
msg green " >> Virtualenv ${VENV_PY_VER} already exists. Activating it and updating packages."
109+
activate "${VENV_PY_VER}"
110+
if (($# > 0)); then
111+
msg green " >> Installing only main project as extra args were specified"
112+
./setup.py install
113+
else
114+
msg green " >> Running pip install -U '.[dev]' ..."
115+
pip install -U '.[dev]'
116+
fi
117+
else
118+
msg green " >> Creating virtualenv at $VENV_PY_VER using python version: ${_CURR_PY_VER[*]}"
119+
mkvenv "$_PYTHON_EXE" "${VENV_PY_VER}"
120+
activate "${VENV_PY_VER}"
121+
msg green " >> [NEW VIRTUALENV] Running pip install -U '.[dev]' ..."
122+
pip install -U '.[dev]'
123+
fi
124+
if (($# > 0)); then
125+
# msg green " >> Installing only main project as extra args were specified"
126+
# pip install -U '.'
127+
msg green " >> Running pytest with args: $* ..."
128+
python3 -m pytest --cov=./privex -rxXs -v "$@"
129+
else
130+
# msg green " >> Running pip install -U '.[dev]' ..."
131+
# pip install -U '.[dev]'
132+
msg green " >> Running pytest ..."
133+
python3 -m pytest --cov=./privex -rxXs -v
134+
fi
135+
msg green " >> Deactivating virtualenv ..."
136+
set +eu
137+
deactivate
138+
set -eu
139+
}
140+
141+
has_command pyenv && HAS_PYENV=1 || HAS_PYENV=0
142+
143+
if ((HAS_PYENV == 1)) && ((USE_PYENV == 1)); then
144+
eval "$(pyenv init -)"
145+
PYENV_AVAIL_VERS=($(pyenv versions --bare))
146+
for v in "${PYENV_VERS[@]}"; do
147+
containsElement "$v" "${PYENV_AVAIL_VERS[@]}" && continue
148+
pyenv_install "$v"
149+
done
150+
for v in "${PYENV_VERS[@]}"; do
151+
msg green " >> Setting shell python version to $v"
152+
export PYENV_VERSION="$v"
153+
_CURR_PY_VER=($(python3 -V))
154+
CURR_PY_VER="${_CURR_PY_VER[1]}"
155+
VENV_PY_VER="venv_pyenv_${CURR_PY_VER}"
156+
_PYTHON_EXE="python3"
157+
main_tests "$@"
158+
done
159+
msg green " >> Clearing pyenv shell variable ..."
160+
unset PYENV_VERSION
161+
else
162+
for v in "${PY_VERS[@]}"; do
163+
if ! has_command "$v" || ! "$v" -V; then
164+
msg red " >> Python version $v is unavailable. Skipping."
165+
continue
166+
fi
167+
_CURR_PY_VER=($("$v" -V))
168+
CURR_PY_VER="${_CURR_PY_VER[1]}"
169+
VENV_PY_VER="venv_py_${CURR_PY_VER}"
170+
_PYTHON_EXE="$v"
171+
main_tests "$@"
172+
# if [[ -d "$VENV_PY_VER" ]]; then
173+
# msg green " >> Virtualenv ${VENV_PY_VER} already exists. Activating it and updating packages."
174+
# activate "${VENV_PY_VER}"
175+
# else
176+
# msg green " >> Creating virtualenv at $VENV_PY_VER using python version: ${_CURR_PY_VER[*]}"
177+
# mkvenv "python3" "${VENV_PY_VER}"
178+
# activate "${VENV_PY_VER}"
179+
# fi
180+
# msg green " >> Running pip install -U '.[dev]' ..."
181+
# pip install -U '.[dev]'
182+
# msg green " >> Running pytest ..."
183+
# python3 -m pytest --cov=./privex -rxXs -v
184+
# msg green " >> Deactivating virtualenv ..."
185+
# deactivate
186+
done
187+
fi

privex/helpers/collections.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@
191191
"""
192192
import inspect
193193
import sys
194-
from collections import namedtuple
194+
from collections import namedtuple, OrderedDict
195195
from typing import Dict, Optional, NamedTuple, Union, Type
196196
import logging
197197

@@ -230,6 +230,25 @@ class DictObject(dict):
230230
{'hello': 'replaced', 'example': 123}
231231
232232
"""
233+
234+
def __getattr__(self, item):
235+
"""When an attribute is requested, e.g. ``x.something``, forward it to ``dict['something']``"""
236+
if hasattr(super(), item):
237+
return super().__getattribute__(item)
238+
return super().__getitem__(item)
239+
240+
def __setattr__(self, key, value):
241+
"""When an attribute is set, e.g. ``x.something = 'abcd'``, forward it to ``dict['something'] = 'abcd'``"""
242+
if hasattr(super(), key):
243+
return super().__setattr__(key, value)
244+
return super().__setitem__(key, value)
245+
246+
247+
class OrderedDictObject(OrderedDict):
248+
"""
249+
Ordered version of :class:`.DictObject` - dictionary with attribute access.
250+
See :class:`.DictObject`
251+
"""
233252
def __getattr__(self, item):
234253
"""When an attribute is requested, e.g. ``x.something``, forward it to ``dict['something']``"""
235254
if hasattr(super(), item):

privex/helpers/common.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from os import getenv as env
3636
from typing import Sequence, List, Union, Tuple, Type, Dict, TypeVar, Any, Iterable, Callable, NewType, Optional
3737

38-
from privex.helpers.collections import DictObject
38+
from privex.helpers.collections import DictObject, OrderedDictObject
3939

4040
log = logging.getLogger(__name__)
4141

@@ -788,7 +788,7 @@ def get_function_params(obj: Union[type, callable], check_parents=False, **kwarg
788788
_cls_keys = inspect.signature(obj).parameters
789789
cls_keys = _filter_params(inspect.signature(obj).parameters, **filter_opts)
790790
if check_parents and hasattr(obj, '__base__') and inspect.isclass(obj):
791-
ret = DictObject({obj: cls_keys})
791+
ret = OrderedDictObject({obj: cls_keys})
792792
last_parent = obj.__base__
793793
while last_parent not in [None, type, object]:
794794
try:
@@ -805,15 +805,15 @@ def get_function_params(obj: Union[type, callable], check_parents=False, **kwarg
805805
continue
806806

807807
if merge:
808-
merged = DictObject()
808+
merged = OrderedDictObject()
809809
for cls in reversed(ret):
810810
for k, p in ret[cls].items():
811811
merged[k] = p
812812
return merged
813813

814814
return ret
815815

816-
return DictObject(cls_keys)
816+
return OrderedDictObject(cls_keys)
817817

818818

819819
def construct_dict(cls: Union[Type[T], C], kwargs: dict, args: Iterable = None, check_parents=True) -> Union[T, Any]:

tests/test_collections.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from typing import Union
4444
from collections import namedtuple, OrderedDict
4545
from privex.helpers import dictable_namedtuple, is_namedtuple, subclass_dictable_namedtuple, \
46-
convert_dictable_namedtuple, DictObject
46+
convert_dictable_namedtuple, DictObject, OrderedDictObject
4747
from tests.base import PrivexBaseCase
4848
import logging
4949

@@ -98,7 +98,57 @@ def test_set_attr(self):
9898
self.assertEqual(x['other_test'], 'example')
9999
self.assertEqual(x.testing, 'testattr')
100100
self.assertEqual(x.other_test, 'example')
101+
102+
103+
class TestOrderedDictObject(PrivexBaseCase):
104+
def test_convert_from_dict(self):
105+
"""Test converting a :class:`dict` into a :class:`.DictObject`"""
106+
x = dict(hello='world', example='testing')
107+
y = OrderedDictObject(x)
108+
self.assertEqual(x, y)
109+
self.assertEqual(y['hello'], 'world')
110+
self.assertEqual(y['example'], 'testing')
111+
self.assertEqual(y.hello, 'world')
112+
self.assertEqual(y.example, 'testing')
113+
114+
def test_convert_to_dict(self):
115+
"""Test converting a :class:`.OrderedDictObject` into a :class:`dict`"""
116+
x = OrderedDictObject(hello='world', example='testing')
117+
y = dict(x)
118+
self.assertEqual(x, y)
119+
self.assertEqual(y['hello'], 'world')
120+
self.assertEqual(y['example'], 'testing')
121+
122+
def test_json_dumps(self):
123+
"""Test serializing a simple :class:`.OrderedDictObject` into JSON"""
124+
x = OrderedDictObject(hello='world', example='testing')
125+
j = json.dumps(x)
126+
self.assertEqual(j, '{"hello": "world", "example": "testing"}')
101127

128+
def test_json_dumps_nested(self):
129+
"""Test serializing a :class:`.OrderedDictObject` with a nested :class:`.OrderedDictObject` into JSON"""
130+
x = OrderedDictObject(hello='world', example='testing')
131+
x.layer = OrderedDictObject(test=True)
132+
j = json.dumps(x)
133+
self.assertEqual(j, '{"hello": "world", "example": "testing", "layer": {"test": true}}')
134+
135+
def test_set_item(self):
136+
"""Test setting a dictionary key via an item/key ``x['y'] = 123``"""
137+
x = OrderedDictObject()
138+
x['testing'] = 'testitem'
139+
self.assertEqual(x['testing'], 'testitem')
140+
self.assertEqual(x.testing, 'testitem')
141+
142+
def test_set_attr(self):
143+
"""Test setting a dictionary key via an attribute ``x.y = 123``"""
144+
x = OrderedDictObject()
145+
x.testing = 'testattr'
146+
x.other_test = 'example'
147+
self.assertEqual(x['testing'], 'testattr')
148+
self.assertEqual(x['other_test'], 'example')
149+
self.assertEqual(x.testing, 'testattr')
150+
self.assertEqual(x.other_test, 'example')
151+
102152

103153
class TestIsNamedTuple(PrivexBaseCase):
104154
"""

0 commit comments

Comments
 (0)