Skip to content

Commit 5f12a6a

Browse files
committed
Work toward Python 3.9+ and Nix support, pytz deprecation
1 parent 6f015b4 commit 5f12a6a

20 files changed

+629
-172
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: ubuntu-latest
2020
strategy:
2121
matrix:
22-
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.x, pypy2, pypy3]
22+
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.x, pypy3]
2323

2424
steps:
2525
- uses: actions/checkout@v2
@@ -32,7 +32,7 @@ jobs:
3232
python -m pip install --upgrade pip
3333
pip install wheel
3434
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
35-
pip install flake8 pytest pytz tzlocal pymodbus pylogix chacha20poly1305 ed25519ll
35+
pip install flake8 pytest pytz tzlocal pymodbus pylogix
3636
pip install 'argparse; python_version < "2.7"'
3737
pip install 'configparser; python_version < "3.0"'
3838
pip install 'ipaddress; python_version < "3.0"'

GNUmakefile

Lines changed: 93 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@
44

55
# PY[3] is the target Python interpreter. It must have pytest installed.
66

7-
PY=python
8-
PY2=python2
9-
PY3=python3
7+
PY ?= python
8+
PY2 ?= python2
9+
PY2_V = $(shell $(PY2) -c "import sys; print('-'.join((next(iter(filter(None,sys.executable.split('/')))),sys.platform,sys.subversion[0].lower(),''.join(map(str,sys.version_info[:2])))))" )
10+
PY3 ?= python3
11+
PY3_V = $(shell $(PY3) -c "import sys; print('-'.join((next(iter(filter(None,sys.executable.split('/')))),sys.platform,sys.implementation.cache_tag)))" 2>/dev/null )
12+
1013

1114
VERSION=$(shell $(PY3) -c 'exec(open("version.py").read()); print( __version__ )')
1215

16+
# TARGET=... nix-shell # CPython version targets: py2, py3{10,11,12,13}
17+
TARGET ?= cpppo_py312
18+
export TARGET
19+
20+
21+
1322
# PY[23]TEST is the desired method of invoking py.test; either as a command, or
1423
# loading it as module, by directly invoking the target Python interpreter.
1524
#
@@ -40,8 +49,22 @@ VERSION=$(shell $(PY3) -c 'exec(open("version.py").read()); print( __version__ )
4049
# Some tests assume the local time-zone is:
4150
TZ=Canada/Mountain
4251

52+
53+
GIT_SSH_COMMAND = ssh -o StrictHostKeyChecking=no
54+
export GIT_SSH_COMMAND
55+
56+
GHUB_NAME = cpppo
57+
GHUB_REPO = [email protected]:pjkundert/$(GHUB_NAME).git
58+
GHUB_BRCH = $(shell git rev-parse --abbrev-ref HEAD )
59+
60+
# We'll agonizingly find the directory above this makefile's path
61+
VENV_DIR = $(abspath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))/.. )
62+
VENV_NAME = $(GHUB_NAME)-$(VERSION)-$(PY3_V)
63+
VENV = $(VENV_DIR)/$(VENV_NAME)
64+
VENV_OPTS =
65+
4366
# To see all pytest output, uncomment --capture=no
44-
PYTESTOPTS=-vv # --capture=no --log-cli-level=25 # INFO # 25 == NORMAL 23 == DETAIL
67+
PYTESTOPTS=-vv --capture=no --log-cli-level=WARNING # INFO # 25 == NORMAL 23 == DETAIL
4568

4669
PY_TEST=TZ=$(TZ) $(PY) -m pytest $(PYTESTOPTS)
4770
PY2TEST=TZ=$(TZ) $(PY2) -m pytest $(PYTESTOPTS)
@@ -72,6 +95,41 @@ help:
7295
@echo " vmware-debian-up Brings up Jessie VM w/ Docker capability"
7396
@echo " vmware-debian-ssh Log in to the VM"
7497

98+
analyze:
99+
$(PY3) -m flake8 --color never -j 1 --max-line-length=250 \
100+
--exclude lib,bin,dist,build,signals,.git \
101+
--ignore=W503,E201,E202,E203,E127,E211,E221,E222,E223,E225,E226,E231,E241,E242,E251,E265,E272,E274,E291 \
102+
103+
pylint:
104+
pylint . --disable=W,C,R
105+
106+
#
107+
# nix-...:
108+
#
109+
# Use a NixOS environment to execute the make target, eg.
110+
#
111+
# nix-venv-activate
112+
#
113+
# The default is the Python 3 crypto_licensing target in default.nix; choose
114+
# TARGET=cpppo_py2 to test under Python 2 (more difficult as time goes on). See default.nix for
115+
# other Python version targets.
116+
#
117+
118+
nix-%:
119+
nix-shell --pure --run "make $*"
120+
121+
122+
#
123+
# test...: Perform Unit Tests
124+
#
125+
# Assumes that the requirements.txt has been installed in the target Python environment. This
126+
# is probably best accomplished by first creating/activating a venv, and then running the test:
127+
#
128+
# $ make nix-venv-activate
129+
# (crypto-licensing-4.0.0) [perry@Perrys-MBP crypto-licensing (feature-py-3.12)]$ make test
130+
# make[1]: Entering directory '/Users/perry/src/crypto-licensing'
131+
# ...
132+
#
75133
test:
76134
$(PY_TEST)
77135
test2:
@@ -101,7 +159,7 @@ pylint:
101159

102160
build3-check:
103161
@$(PY3) -m build --version \
104-
|| ( echo "\n*** Missing Python modules; run:\n\n $(PY3) -m pip install --upgrade pip setuptools build\n" \
162+
|| ( echo "\n*** Missing Python modules; run:\n\n $(PY3) -m pip install --upgrade -r requirements-dev.txt\n" \
105163
&& false )
106164

107165
build3: build3-check clean
@@ -119,6 +177,8 @@ install3: dist/cpppo-$(VERSION)-py3-none-any.whl
119177
install23: install2 install3
120178
install: install3
121179

180+
install-%: # ...-dev, -tests
181+
$(PY3) -m pip install --upgrade -r requirements-$*.txt
122182

123183
# Support uploading a new version of cpppo to pypi. Must:
124184
# o advance __version__ number in cpppo/version.py
@@ -130,45 +190,6 @@ upload: clean
130190
clean:
131191
@rm -rf MANIFEST *.png build dist auto *.egg-info $(shell find . -name '*.pyc' -o -name '__pycache__' )
132192

133-
# Virtualization management, eg:
134-
# make {vmware,vagrant}-up/halt/ssh/destroy
135-
#
136-
# To use a different Vagrant box than precise64 (eg. raring), Vagrantfile must be altered
137-
.PHONY: vagrant vagrant_boxes \
138-
precise64_virtualbox precise64_vmware_fusion \
139-
raring_virtualbox
140-
141-
# The vagrant/ubuntu/Vagrantfile doesn't contain a config.vm.box_url; we must
142-
# supply. The precise64 VMware image presently supports only VMware Fusion 5;
143-
# if you see an error regarding hgfs kernel modules, you may be running a
144-
# version of VMware Fusion incompatible with the VMware Tools in the image.
145-
# TODO: remove; no longer supported.
146-
vmware-ubuntu-%: precise64-vmware_fusion
147-
cd vagrant/ubuntu; vagrant $* $(if $(filter up, $*), --provider=vmware_fusion,)
148-
149-
virtualbox-ubuntu-%: precise64-virtualbox
150-
cd vagrant/ubuntu; vagrant $* $(if $(filter up, $*), --provider=virtualbox,)
151-
152-
# The jessie64 VMware image is compatible with VMware Fusion 6, and the VirtualBox image is
153-
# compatible with VirtualBox 4.3. Obtains the box, if necessary. The packer.io generated VMware
154-
# boxes identify themselves as being for vmware_desktop; these are compatible with vmware_fusion
155-
vmware-debian-%: jessie64-vmware_desktop
156-
cd vagrant/debian; vagrant $* $(if $(filter up, $*), --provider=vmware_fusion,)
157-
158-
virtualbox-debian-%: jessie64-virtualbox
159-
cd vagrant/debian; vagrant $* $(if $(filter up, $*), --provider=virtualbox,)
160-
161-
vagrant:
162-
@vagrant --help >/dev/null || ( echo "Install vagrant: http://vagrantup.com"; exit 1 )
163-
164-
165-
# Check if jessie64-{virtualbox,vmware_desktop} exists in the vagrant box list.
166-
# If not, install it.
167-
jessie64-%:
168-
@if ! vagrant box list | grep -q '^jessie64.*($*'; then \
169-
vagrant box add jessie64 http://box.hardconsulting.com/jessie64-$*.box --provider $*; \
170-
fi
171-
172193

173194
# Run only tests with a prefix containing the target string, eg test-blah
174195
test-%:
@@ -192,6 +213,34 @@ unit23-%:
192213
$(PY3TEST) -k $*
193214

194215

216+
#
217+
# venv: Create a Virtual Env containing the installed repo
218+
#
219+
.PHONY: venv venv-activate.sh venv-activate
220+
venv: $(VENV)
221+
venv-activate.sh: $(VENV)/venv-activate.sh
222+
venv-activate: $(VENV)/venv-activate.sh
223+
@echo; echo "*** Activating $< VirtualEnv for Interactive $(SHELL)"
224+
@bash --init-file $< -i
225+
# Create the venv, and then install cpppo from the current directory
226+
$(VENV):
227+
@echo; echo "*** Building $@ VirtualEnv..."
228+
@rm -rf $@ && $(PY3) -m venv $(VENV_OPTS) $@ \
229+
&& source $@/bin/activate \
230+
&& make install-dev install
231+
232+
# Activate a given VirtualEnv, and go to its routeros_ssh installation
233+
# o Creates a custom venv-activate.sh script in the venv, and uses it start
234+
# start a sub-shell in that venv, with a CWD in the contained routeros_ssh installation
235+
$(VENV)/venv-activate.sh: $(VENV)
236+
( \
237+
echo "PS1='[\u@\h \W)]\\$$ '"; \
238+
echo "[ -r ~/.git-completion.bash ] && source ~/.git-completion.bash"; \
239+
echo "[ -r ~/.git-prompt.sh ] && source ~/.git-prompt.sh && PS1='[\u@\h \W\$$(__git_ps1 \" (%s)\")]\\$$ '"; \
240+
echo "source $</bin/activate"; \
241+
) > $@
242+
243+
195244
#
196245
# Target to allow the printing of 'make' variables, eg:
197246
#

bin/modbus_poll.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ def main():
4242
Begin polling the designated register range(s), optionally writing initial values to them.
4343
4444
Register range(s) and value(s) must be supplied:
45-
45+
4646
<begin>[-<end>]
4747
<begin>[-<end>]=<val>,...
48-
48+
4949
EXAMPLE
50-
50+
5151
modbus_poll --address localhost:7502 40001-40100
52-
52+
5353
""" )
5454
parser.add_argument( '-v', '--verbose',
5555
default=0, action="count", help="Display logging information." )
@@ -59,13 +59,15 @@ def main():
5959
help="Default [interface][:port] to bind to (default: any, port 502)" )
6060
parser.add_argument( '-r', '--reach', default=1,
6161
help="Merge polls within <reach> registers of each-other" )
62+
parser.add_argument( '-u', '--unit', default=None,
63+
help="Specify a Modbus device number (default: %s)" % ( Defaults.UnitId ))
6264
parser.add_argument( '-R', '--rate', default=1.0,
6365
help="Target poll rate" )
6466
parser.add_argument( '-t', '--timeout', default=Defaults.Timeout,
6567
help="I/O Timeout (default: %s)" % ( Defaults.Timeout ))
6668
parser.add_argument( 'registers', nargs="+" )
6769
args = parser.parse_args()
68-
70+
6971
# Deduce logging level and target file (if any)
7072
levelmap = {
7173
0: logging.WARNING,
@@ -98,7 +100,13 @@ def main():
98100

99101
# Start the PLC poller (and perform any initial writes indicated)
100102
poller = poller_modbus(
101-
"Modbus/TCP", host=address[0], port=address[1], reach=int( args.reach ), rate=float( args.rate ))
103+
"Modbus/TCP",
104+
host = address[0],
105+
port = address[1],
106+
reach = int( args.reach ),
107+
rate = float( args.rate ),
108+
unit = int( args.unit ) if args.unit else None
109+
)
102110

103111
for txt in args.registers:
104112
beg,end,val = register_decode( txt ) # beg-end is inclusive
@@ -112,7 +120,7 @@ def main():
112120
for base,length in merge( [ (beg,end-beg+1) ] ):
113121
poller.write( base, val[0] if length == 1 else val[:length] )
114122
val = val[length:]
115-
123+
116124
load = ''
117125
fail = ''
118126
poll = ''

bin/modbus_sim.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ def register_parse( txt ):
120120
"""Tokenizer yields integers; any other character (except space), one at a time. """
121121
b, i, e = 0, 0, len( txt )
122122
while i <= e:
123-
if i == e or not( '0' <= txt[i] <= '9' ):
123+
if i == e or not( '0' <= txt[i] <= '9' or txt[i].lower() in ('x','a','b','c','d','e','f') ):
124124
if i > b:
125-
yield int( txt[b:i] ) # "123..." Parsed 1+ digits
125+
v = txt[b:i]
126+
yield int( v, 16 if 'x' in v.lower() else 10 ) # "123..." Parsed 1+ digits
126127
b, i = i, i-1
127128
elif i < e:
128129
if txt[b] != ' ':

default.nix

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{ pkgs ? import ./nixpkgs.nix {} }:
2+
3+
with pkgs;
4+
5+
let
6+
in
7+
{
8+
cpppo_py313 = stdenv.mkDerivation rec {
9+
name = "python313-with-pytest";
10+
11+
buildInputs = [
12+
git
13+
openssh
14+
python313
15+
python313Packages.pytest
16+
];
17+
};
18+
19+
cpppo_py312 = stdenv.mkDerivation rec {
20+
name = "python312-with-pytest";
21+
22+
buildInputs = [
23+
git
24+
openssh
25+
python312
26+
python312Packages.pytest
27+
];
28+
};
29+
30+
cpppo_py311 = stdenv.mkDerivation rec {
31+
name = "python311-with-pytest";
32+
33+
buildInputs = [
34+
git
35+
openssh
36+
python311
37+
python311Packages.pytest
38+
];
39+
};
40+
41+
cpppo_py310 = stdenv.mkDerivation rec {
42+
name = "python310-with-pytest";
43+
44+
buildInputs = [
45+
git
46+
openssh
47+
python310
48+
python310Packages.pytest
49+
];
50+
};
51+
52+
cpppo_py2 = stdenv.mkDerivation rec {
53+
name = "python2-with-pytest";
54+
55+
buildInputs = [
56+
git
57+
openssh
58+
python27
59+
python27Packages.pytest
60+
python27Packages.pip
61+
];
62+
};
63+
}

dotdict.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -467,30 +467,33 @@ class apidict( apidict_base ):
467467
#
468468
# To use apidict via multiprocessing.Process, we can proxy the API -- but these proxies cannot
469469
# successfully proxy __getattr__/__setattr__. So, users must employ set/get instead;
470-
# .set/.setdefault will block 'til a counterparty executes .get().
470+
# .set/.setdefault will block 'til a counterparty executes .get(). Uses the Iterator type, so must
471+
# be registered w/ SyncManager which knows of that type.
471472
#
473+
# EXAMPLE:
474+
# >>> multiprocessing.managers.SyncManager.register( *make_apidict_proxy( apidict ))
475+
472476
def make_apidict_proxy( apidict_class ):
473477
"""Product a tuple usable to call SyncManager.register, for the given apidict derived class.
474-
Supplies its __name__ as "<name>_proxy" for the proxy, and registers the bare "<name>" with the
478+
Supplies its __name__ as "<name>_proxy" for the proxy, and registers this <name>_proxy with the
475479
multiprocessing Manager.
476480
477481
"""
482+
apidict_class_name = apidict_class.__name__ # + '_proxy'
478483
apidict_proxy = multiprocessing.managers.MakeProxyType(
479-
apidict_class.__name__ + '_proxy', (
484+
apidict_class_name, (
480485
'__contains__', '__delitem__', '__getitem__', '__iter__', '__len__',
481486
'__setitem__', 'clear', 'copy', 'get', 'set'
482487
# These will not work in python3, as they return generators; use list( <apidict> )
483488
# instead, as the __iter__ method is supported and returns the keys.
484489
'items', 'keys',
485490
'pop', 'popitem', 'setdefault', 'update', 'values'
486491
'__dir__', 'set', # '__setattr__', '__getattr__',
487-
# 'iteritems', 'iterkeys', 'itervalues', # These cannot be proxied, as they return generations
492+
# 'iteritems', 'iterkeys', 'itervalues', # These cannot be proxied, as they return generators
488493
'listitems', 'listkeys', 'listvalues',
489494
)
490495
)
491496
apidict_proxy._method_to_typeid_ = {
492497
'__iter__': 'Iterator',
493498
}
494-
return apidict_class.__name__, apidict_class, apidict_proxy
495-
496-
multiprocessing.managers.SyncManager.register( *make_apidict_proxy( apidict ))
499+
return apidict_class_name, apidict_class, apidict_proxy

0 commit comments

Comments
 (0)