Skip to content

Commit e1ec7b3

Browse files
committed
pymas fix for python files
1 parent 65107c2 commit e1ec7b3

File tree

4 files changed

+164
-44
lines changed

4 files changed

+164
-44
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Unreleased
22
----------
3-
-
3+
- Fixed PyMAS utilities to correctly work functions not bound to pickled objects.
44

55
v1.5 (2020-2-23)
66
----------------

src/sasctl/utils/misc.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
# Copyright © 2019, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
55
# SPDX-License-Identifier: Apache-2.0
66

7+
import random
8+
import string
9+
10+
from .decorators import versionadded
11+
712

813
def installed_packages():
914
"""List Python packages installed in the current environment.
@@ -25,4 +30,21 @@ def installed_packages():
2530
freeze = None
2631

2732
if freeze is not None:
28-
return list(freeze.freeze())
33+
return list(freeze.freeze())
34+
35+
36+
@versionadded(version='1.5.1')
37+
def random_string(length):
38+
"""Generates a random alpha-numeric string of a given length.
39+
40+
Parameters
41+
----------
42+
length : int
43+
The length of the generate string.
44+
45+
Returns
46+
-------
47+
str
48+
49+
"""
50+
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))

src/sasctl/utils/pymas/core.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@
99
from __future__ import print_function
1010
import base64
1111
import importlib
12-
import pickle # skipcq BAN-B301
12+
import pickle # skipcq BAN-B301
1313
import os
1414
import re
1515
import sys
1616
from collections import OrderedDict
17-
1817
import six
1918

2019
from .ds2 import DS2Thread, DS2Variable, DS2PyMASPackage
2120
from .python import ds2_variables
2221
from ..decorators import versionadded, versionchanged
22+
from ..misc import random_string
2323

2424

2525
@versionchanged(reason='Added `name` parameter.', version='1.5')
@@ -88,19 +88,18 @@ def build_wrapper_function(func, variables, array_input,
8888
string_input += (" if {0}: {0} = {0}.strip()".format(v.name), )
8989
else:
9090
string_input += (
91-
" if {0} is None: {0} = np.nan".format(v.name),)
92-
91+
" if {0} is None: {0} = np.nan".format(v.name),)
9392

9493
# Statement to execute the function w/ provided parameters
9594
if array_input:
9695
middle = string_input + \
9796
(
98-
' input_array = np.array([{}]).reshape((1, -1))'.format(
97+
' input_array = np.array([{}]).reshape((1, -1))'.format(
9998
', '.join(args)),
100-
' columns = [{}]'.format(
99+
' columns = [{}]'.format(
101100
', '.join('"{0}"'.format(w) for w in args)),
102-
' input_df = pd.DataFrame(data=input_array, columns=columns)',
103-
' result = {}(input_df)'.format(func))
101+
' input_df = pd.DataFrame(data=input_array, columns=columns)',
102+
' result = {}(input_df)'.format(func))
104103
else:
105104
func_call = '{}({})'.format(func, ','.join(args))
106105
middle = (' result = {}'.format(func_call),)
@@ -131,8 +130,8 @@ def build_wrapper_function(func, variables, array_input,
131130
' import pandas as pd') + \
132131
middle + \
133132
(
134-
' result = tuple(result.ravel()) if hasattr(result, '
135-
'"ravel") else tuple(result)',
133+
' result = tuple(result.ravel()) if hasattr(result, '
134+
'"ravel") else tuple(result)',
136135
' if len(result) == 0:',
137136
' result = tuple(None for i in range(%s))' % len(
138137
output_names),
@@ -142,9 +141,9 @@ def build_wrapper_function(func, variables, array_input,
142141
' from traceback import format_exc',
143142
' msg = str(e) + format_exc()' if return_msg else '',
144143
' if result is None:',
145-
' result = tuple(None for i in range(%s))' % len(
144+
' result = tuple(None for i in range(%s))' % len(
146145
output_names),
147-
' return result + (msg, )')
146+
' return result + (msg, )')
148147

149148
return '\n'.join(definition)
150149

@@ -158,7 +157,7 @@ def wrap_predict_method(func, variables, **kwargs):
158157
func : function or str
159158
Function name or an instance of Function which will be wrapped. Assumed
160159
to behave as `.predict()` methods.
161-
variables : list of DS2Variable
160+
variables : list of DS2Variable
162161
Input and output variables for the function
163162
kwargs : any
164163
Will be passed to `build_wrapper_function`.
@@ -188,7 +187,7 @@ def wrap_predict_proba_method(func, variables, **kwargs):
188187
func : function or str
189188
Function name or an instance of Function which will be wrapped. Assumed
190189
to behave as `.predict_proba()` methods.
191-
variables : list of DS2Variable
190+
variables : list of DS2Variable
192191
Input and output variables for the function
193192
kwargs : any
194193
Will be passed to `build_wrapper_function`.
@@ -312,8 +311,7 @@ def from_python_file(file, func_name=None, input_types=None, array_input=False,
312311
with open(file, 'r') as f:
313312
code = [line.strip('\n') for line in f.readlines()]
314313

315-
return _build_pymas(target_func, None, input_types, array_input,
316-
return_code, return_message, code)
314+
return _build_pymas(target_func, None, input_types, array_input, code=code)
317315

318316

319317
@versionchanged('Return code and message are disabled by default.', version='1.5')
@@ -382,11 +380,12 @@ def from_pickle(file, func_name=None, input_types=None, array_input=False,
382380
'bytes = {}'.format(pkl).replace("'", '"'),
383381
'obj = pickle.loads(base64.b64decode(bytes))')
384382

385-
return _build_pymas(obj, func_name, input_types, array_input, return_code,
386-
return_message, code)
383+
return _build_pymas(obj, func_name, input_types, array_input,
384+
func_prefix='obj.', code=code)
387385

388386

389387
def _build_pymas(obj, func_name=None, input_types=None, array_input=False,
388+
func_prefix=None,
390389
return_code=None, return_message=None, code=None):
391390
"""
392391
@@ -465,7 +464,7 @@ def parse_function(obj, func_name):
465464
variables = list(variables)
466465

467466
return PyMAS(target_func, variables, code,
468-
array_input=array_input, func_prefix='obj.')
467+
array_input=array_input, func_prefix=func_prefix)
469468

470469

471470
class PyMAS:
@@ -521,17 +520,23 @@ def __init__(self,
521520
return_msg = [return_msg] * len(target_function)
522521

523522
self.wrapper = []
523+
wrapper_names = []
524524
for func, vars, code, msg in zip(target_function, variables, return_code, return_msg):
525+
wrapper_names.append('_' + random_string(20))
526+
525527
if func.lower() == 'predict':
526528
lines = wrap_predict_method(func_prefix + func, vars,
527-
setup=python_source, **kwargs)
529+
setup=python_source,
530+
name=wrapper_names[-1],
531+
**kwargs)
528532
elif func.lower() == 'predict_proba':
529533
lines = wrap_predict_proba_method(func_prefix + func, vars,
534+
name=wrapper_names[-1],
530535
setup=python_source, **kwargs)
531536
else:
532537
lines = build_wrapper_function(func_prefix+func,
533538
vars,
534-
name=func,
539+
name=wrapper_names[-1],
535540
setup=python_source,
536541
**kwargs)
537542

@@ -556,7 +561,8 @@ def __init__(self,
556561
self.package = DS2PyMASPackage(self.wrapper)
557562

558563
for idx, func in enumerate(target_function):
559-
self.package.add_method(func, func,
564+
self.package.add_method(func,
565+
wrapper_names[idx],
560566
variables[idx])
561567

562568
@versionchanged(version='1.4', reason="Added `dest='Python'` option")

tests/integration/test_pymas.py

Lines changed: 112 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,14 @@ def test_from_pickle(train_data, pickle_file):
140140

141141
X, y = train_data
142142

143-
with mock.patch('uuid.uuid4') as mocked:
144-
mocked.return_value.hex = 'DF74A4B18C9E41A2A34B0053E123AA67'
145-
p = from_pickle(pickle_file,
146-
func_name='predict',
147-
input_types=X,
148-
array_input=True)
143+
with mock.patch('sasctl.utils.pymas.core.random_string') as mock_rnd_string:
144+
mock_rnd_string.return_value = 'randomMethodName'
145+
with mock.patch('uuid.uuid4') as mocked:
146+
mocked.return_value.hex = 'DF74A4B18C9E41A2A34B0053E123AA67'
147+
p = from_pickle(pickle_file,
148+
func_name='predict',
149+
input_types=X,
150+
array_input=True)
149151

150152
target = """
151153
package _DF74A4B18C9E41A2A34B0053E123AA6 / overwrite=yes;
@@ -169,7 +171,7 @@ def test_from_pickle(train_data, pickle_file):
169171
rc = py.appendSrcLine('except Exception as e:');
170172
rc = py.appendSrcLine(' _compile_error = e');
171173
rc = py.appendSrcLine('');
172-
rc = py.appendSrcLine('def predict(SepalLength, SepalWidth, PetalLength, PetalWidth):');
174+
rc = py.appendSrcLine('def _randomMethodName(SepalLength, SepalWidth, PetalLength, PetalWidth):');
173175
rc = py.appendSrcLine(' "Output: var1, msg"');
174176
rc = py.appendSrcLine(' result = None');
175177
rc = py.appendSrcLine(' msg = None');
@@ -219,7 +221,7 @@ def test_from_pickle(train_data, pickle_file):
219221
220222
dcl integer rc;
221223
dcl varchar(4068) msg;
222-
rc = py.useMethod('predict');
224+
rc = py.useMethod('_randomMethodName');
223225
if rc then return;
224226
rc = py.setDouble('SepalLength', SepalLength); if rc then return;
225227
rc = py.setDouble('SepalWidth', SepalWidth); if rc then return;
@@ -251,10 +253,12 @@ def test_from_pickle_2(train_data, pickle_file):
251253

252254
X, _ = train_data
253255

254-
with mock.patch('uuid.uuid4') as mocked:
255-
mocked.return_value.hex = 'DF74A4B18C9E41A2A34B0053E123AA67'
256-
p = from_pickle(pickle_file, func_name=['predict', 'predict_proba'],
257-
input_types=X, array_input=True)
256+
with mock.patch('sasctl.utils.pymas.core.random_string') as mock_rnd_string:
257+
mock_rnd_string.side_effect = ['randomMethodName1', 'randomMethodName2']
258+
with mock.patch('uuid.uuid4') as mocked:
259+
mocked.return_value.hex = 'DF74A4B18C9E41A2A34B0053E123AA67'
260+
p = from_pickle(pickle_file, func_name=['predict', 'predict_proba'],
261+
input_types=X, array_input=True)
258262

259263
target = """
260264
package _DF74A4B18C9E41A2A34B0053E123AA6 / overwrite=yes;
@@ -278,7 +282,7 @@ def test_from_pickle_2(train_data, pickle_file):
278282
rc = py.appendSrcLine('except Exception as e:');
279283
rc = py.appendSrcLine(' _compile_error = e');
280284
rc = py.appendSrcLine('');
281-
rc = py.appendSrcLine('def predict(SepalLength, SepalWidth, PetalLength, PetalWidth):');
285+
rc = py.appendSrcLine('def _randomMethodName1(SepalLength, SepalWidth, PetalLength, PetalWidth):');
282286
rc = py.appendSrcLine(' "Output: var1, msg"');
283287
rc = py.appendSrcLine(' result = None');
284288
rc = py.appendSrcLine(' msg = None');
@@ -309,7 +313,7 @@ def test_from_pickle_2(train_data, pickle_file):
309313
rc = py.appendSrcLine(' result = tuple(None for i in range(1))');
310314
rc = py.appendSrcLine(' return result + (msg, )');
311315
rc = py.appendSrcLine('');
312-
rc = py.appendSrcLine('def predict_proba(SepalLength, SepalWidth, PetalLength, PetalWidth):');
316+
rc = py.appendSrcLine('def _randomMethodName2(SepalLength, SepalWidth, PetalLength, PetalWidth):');
313317
rc = py.appendSrcLine(' "Output: P_1, P_2, P_3, msg"');
314318
rc = py.appendSrcLine(' result = None');
315319
rc = py.appendSrcLine(' msg = None');
@@ -359,7 +363,7 @@ def test_from_pickle_2(train_data, pickle_file):
359363
360364
dcl integer rc;
361365
dcl varchar(4068) msg;
362-
rc = py.useMethod('predict');
366+
rc = py.useMethod('_randomMethodName1');
363367
if rc then return;
364368
rc = py.setDouble('SepalLength', SepalLength); if rc then return;
365369
rc = py.setDouble('SepalWidth', SepalWidth); if rc then return;
@@ -383,7 +387,7 @@ def test_from_pickle_2(train_data, pickle_file):
383387
384388
dcl integer rc;
385389
dcl varchar(4068) msg;
386-
rc = py.useMethod('predict_proba');
390+
rc = py.useMethod('_randomMethodName2');
387391
if rc then return;
388392
rc = py.setDouble('SepalLength', SepalLength); if rc then return;
389393
rc = py.setDouble('SepalWidth', SepalWidth); if rc then return;
@@ -419,11 +423,99 @@ def test_from_pickle_stream(train_data, pickle_stream):
419423
assert isinstance(p, PyMAS)
420424

421425

422-
def test_from_python_file(python_file):
423-
from sasctl.utils.pymas import PyMAS, from_python_file
426+
def test_from_python_file(tmpdir):
427+
from sasctl.utils.pymas import from_python_file
424428

425-
p = from_python_file(python_file, func_name='predict')
426-
assert isinstance(p, PyMAS)
429+
code = """
430+
def hello_world():
431+
print('Hello World!')
432+
433+
"""
434+
435+
target = """
436+
package _DF74A4B18C9E41A2A34B0053E123AA6 / overwrite=yes;
437+
dcl package pymas py;
438+
dcl package logger logr('App.tk.MAS');
439+
dcl varchar(67108864) character set utf8 pycode;
440+
dcl int revision;
441+
442+
method init();
443+
444+
dcl integer rc;
445+
if null(py) then do;
446+
py = _new_ pymas();
447+
rc = py.useModule('DF74A4B18C9E41A2A34B0053E123AA67', 1);
448+
if rc then do;
449+
rc = py.appendSrcLine('try:');
450+
rc = py.appendSrcLine(' ');
451+
rc = py.appendSrcLine(' def hello_world():');
452+
rc = py.appendSrcLine(' print('Hello World!')');
453+
rc = py.appendSrcLine(' ');
454+
rc = py.appendSrcLine(' _compile_error = None');
455+
rc = py.appendSrcLine('except Exception as e:');
456+
rc = py.appendSrcLine(' _compile_error = e');
457+
rc = py.appendSrcLine('');
458+
rc = py.appendSrcLine('def _randomMethodName():');
459+
rc = py.appendSrcLine(' "Output: result, msg"');
460+
rc = py.appendSrcLine(' result = None');
461+
rc = py.appendSrcLine(' msg = None');
462+
rc = py.appendSrcLine(' try:');
463+
rc = py.appendSrcLine(' global _compile_error');
464+
rc = py.appendSrcLine(' if _compile_error is not None:');
465+
rc = py.appendSrcLine(' raise _compile_error');
466+
rc = py.appendSrcLine(' import numpy as np');
467+
rc = py.appendSrcLine(' import pandas as pd');
468+
rc = py.appendSrcLine(' result = hello_world()');
469+
rc = py.appendSrcLine(' result = tuple(result.ravel()) if hasattr(result, "ravel") else tuple(result)');
470+
rc = py.appendSrcLine(' if len(result) == 0:');
471+
rc = py.appendSrcLine(' result = tuple(None for i in range(1))');
472+
rc = py.appendSrcLine(' elif "numpy" in str(type(result[0])):');
473+
rc = py.appendSrcLine(' result = tuple(np.asscalar(i) for i in result)');
474+
rc = py.appendSrcLine(' except Exception as e:');
475+
rc = py.appendSrcLine(' from traceback import format_exc');
476+
rc = py.appendSrcLine(' msg = str(e) + format_exc()');
477+
rc = py.appendSrcLine(' if result is None:');
478+
rc = py.appendSrcLine(' result = tuple(None for i in range(1))');
479+
rc = py.appendSrcLine(' return result + (msg, )');
480+
pycode = py.getSource();
481+
revision = py.publish(pycode, 'DF74A4B18C9E41A2A34B0053E123AA67');
482+
if revision lt 1 then do;
483+
logr.log('e', 'py.publish() failed.');
484+
rc = -1;
485+
end;
486+
end;
487+
end;
488+
end;
489+
490+
method hello_world(
491+
in_out double result
492+
);
493+
494+
dcl integer rc;
495+
dcl varchar(4068) msg;
496+
rc = py.useMethod('_randomMethodName');
497+
if rc then return;
498+
rc = py.execute(); if rc then return;
499+
result = py.getDouble('result');
500+
msg = py.getString('msg');
501+
if not null(msg) then logr.log('e', 'Error executing Python function "hello_world": $s', msg);
502+
end;
503+
504+
endpackage;
505+
""".lstrip('\n')
506+
507+
f = tmpdir.join('model.py')
508+
f.write(code)
509+
510+
with mock.patch('sasctl.utils.pymas.core.random_string') as mock_rnd_string:
511+
mock_rnd_string.return_value = 'randomMethodName'
512+
with mock.patch('uuid.uuid4') as mocked:
513+
mocked.return_value.hex = 'DF74A4B18C9E41A2A34B0053E123AA67'
514+
p = from_python_file(str(f), 'hello_world')
515+
516+
result = p.score_code()
517+
518+
assert result == target
427519

428520

429521
def test_with_sklearn_pipeline(train_data, sklearn_pipeline):

0 commit comments

Comments
 (0)