Skip to content

Commit bfe048e

Browse files
d-w-mooretrel
authored andcommitted
[#336] rule files can now be submitted from a memory file object
Updated tests, doc strings and README to reflect changes.
1 parent c669336 commit bfe048e

3 files changed

Lines changed: 105 additions & 11 deletions

File tree

README.rst

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -503,13 +503,18 @@ irods server log::
503503
r = irods.rule.Rule( session, rule_file = 'native1.r')
504504
r.execute()
505505

506-
Release v1.1.0 includes the ability to target a specific rule engine instance by name, so if other rule engine instances are present, we may want
507-
to instantiate using::
508-
509-
Rule( ... , instance_name = 'irods_rule_engine_plugin-irods_rule_language-instance' )
510-
511-
Additionally, if we wanted to have the :code:`native1.r` rule code print to stdout instead, we could also set :code:`*stream` in the :code:`INPUT`
512-
parameters for the rule and change the :code:`OUTPUT` parameter from :code:`null` to :code:`ruleExecOut` to accommodate the output stream::
506+
With release v1.1.1, not only can we target a specific rule engine instance by name (which is useful when
507+
more than one is present), but we can also use a file-like object for the :code:`rule_file` parameter::
508+
509+
Rule( session, rule_file = io.StringIO(u'''mainRule() { anotherRule(*x); writeLine('stdout',*x) }\n'''
510+
u'''anotherRule(*OUT) {*OUT='hello world!'}\n\n'''
511+
u'''OUTPUT ruleExecOut\n'''),
512+
instance_name = 'irods_rule_engine_plugin-irods_rule_language-instance' )
513+
514+
Incidentally, if we wanted to change the :code:`native1.r` rule code print to stdout also, we could set the
515+
:code:`INPUT` parameter, :code:`*stream`, using the Rule constructor's :code:`params` keyword argument.
516+
Similarly, we can change the :code:`OUTPUT` parameter from :code:`null` to :code:`ruleExecOut`, to accommodate
517+
the output stream, via the :code:`output` argument::
513518

514519
r = irods.rule.Rule( session, rule_file = 'native1.r',
515520
instance_name = 'irods_rule_engine_plugin-irods_rule_language-instance',

irods/rule.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import irods.exception as ex
55
from io import open as io_open
66
from irods.message import Message, StringProperty
7+
import six
78

89
class RemoveRuleMessage(Message):
910
#define RULE_EXEC_DEL_INP_PI "str ruleExecId[NAME_LEN];"
@@ -20,8 +21,11 @@ def __init__(self, session, rule_file=None, body='', params=None, output='', ins
2021
2122
Arguments:
2223
Use one of:
23-
* rule_file : the name of an existing rule script ending in '.r' and containing iRODS rules
24-
* body: the text of the rule code or rule call(s) to be run.
24+
* rule_file : the name of an existing file containint "rule script" style code. In the context of
25+
the native iRODS Rule Language, this is a file ending in '.r' and containing iRODS rules.
26+
Optionally, this parameter can be a file-like object containing the rule script text.
27+
* body: the text of block of rule code (possibly including rule calls) to be run as if it were
28+
the body of a rule, e.g. the part between the braces of a rule definition in the iRODS rule language.
2529
* instance_name: the name of the rule engine instance in the context of which to run the rule(s).
2630
* output may be set to 'ruleExecOut' if console output is expected on stderr or stdout streams.
2731
* params are key/value pairs to be sent into a rule_file.
@@ -62,11 +66,37 @@ def remove_by_id(self,*ids):
6266
raise RuntimeError("Error removing rule {id_}".format(**locals()))
6367

6468
def load(self, rule_file, encoding = 'utf-8'):
69+
"""Load rule code with rule-file (*.r) semantics.
70+
71+
A "main" rule is defined first; name does not matter. Other rules may follow, which will be
72+
callable from the first rule. Any rules defined in active rule-bases within the server are
73+
also callable.
74+
75+
The `rule_file' parameter is a filename or file-like object. We give it either:
76+
- a string holding the path to a rule-file in the local filesystem, or
77+
- an in-memory object (eg. io.StringIO or io.BytesIO) whose content is that of a rule-file.
78+
79+
This addresses a regression in v1.1.0; see issue #336. In v1.1.1+, if rule code is passed in literally via
80+
the `body' parameter of the Rule constructor, it is interpreted as if it were the body of a rule, and
81+
therefore it may not contain internal rule definitions. However, if rule code is submitted as the content
82+
of a file or file-like object referred to by the `rule_file' parameter of the Rule constructor, will be
83+
interpreted as .r-file content. Therefore, it must contain a main rule definition first, followed
84+
possibly by others which will be callable from the main rule as if they were part of the core rule-base.
85+
86+
"""
6587
self.body = '@external\n'
6688

67-
# parse rule file
68-
with io_open(rule_file, encoding = encoding) as f:
89+
90+
with (io_open(rule_file, encoding = encoding) if isinstance(rule_file,six.string_types) else rule_file
91+
) as f:
92+
93+
# parse rule file line-by-line
6994
for line in f:
95+
96+
# convert input line to Unicode if necessary
97+
if isinstance(line, bytes):
98+
line = line.decode(encoding)
99+
70100
# parse input line
71101
if line.strip().lower().startswith('input'):
72102

irods/test/rule_test.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from irods.rule import Rule
1313
import six
1414
from io import open as io_open
15+
import io
1516

1617

1718
RE_Plugins_installed_run_condition_args = ( os.environ.get('PYTHON_RULE_ENGINE_INSTALLED','*').lower()[:1]=='y',
@@ -341,6 +342,64 @@ def test_retrieve_std_streams_from_rule(self):
341342
os.remove(rule_file_path)
342343

343344

345+
@staticmethod
346+
def lines_from_stdout_buf(output):
347+
buf = ""
348+
if output and len(output.MsParam_PI):
349+
buf = output.MsParam_PI[0].inOutStruct.stdoutBuf.buf
350+
if buf:
351+
buf = buf.rstrip(b'\0').decode('utf8')
352+
return buf.splitlines()
353+
354+
355+
def test_rulefile_in_file_like_object_1__336(self):
356+
357+
rule_file_contents = textwrap.dedent(u"""\
358+
hw {
359+
helloWorld(*message);
360+
writeLine("stdout", "Message is: [*message] ...");
361+
}
362+
helloWorld(*OUT)
363+
{
364+
*OUT = "Hello world!"
365+
}
366+
""")
367+
r = Rule(self.sess, rule_file = io.StringIO( rule_file_contents ),
368+
output = 'ruleExecOut', instance_name='irods_rule_engine_plugin-irods_rule_language-instance')
369+
output = r.execute()
370+
lines = self.lines_from_stdout_buf(output)
371+
self.assertRegexpMatches (lines[0], '.*\[Hello world!\]')
372+
373+
374+
def test_rulefile_in_file_like_object_2__336(self):
375+
376+
rule_file_contents = textwrap.dedent("""\
377+
main {
378+
other_rule()
379+
writeLine("stdout","["++type(*msg2)++"][*msg2]");
380+
}
381+
other_rule {
382+
writeLine("stdout","["++type(*msg1)++"][*msg1]");
383+
}
384+
385+
INPUT *msg1="",*msg2=""
386+
OUTPUT ruleExecOut
387+
""")
388+
389+
r = Rule(self.sess, rule_file = io.BytesIO( rule_file_contents.encode('utf-8') ))
390+
output = r.execute()
391+
lines = self.lines_from_stdout_buf(output)
392+
self.assertRegexpMatches (lines[0], '\[STRING\]\[\]')
393+
self.assertRegexpMatches (lines[1], '\[STRING\]\[\]')
394+
395+
r = Rule(self.sess, rule_file = io.BytesIO( rule_file_contents.encode('utf-8') )
396+
, params = {'*msg1':5, '*msg2':'"A String"'})
397+
output = r.execute()
398+
lines = self.lines_from_stdout_buf(output)
399+
self.assertRegexpMatches (lines[0], '\[INTEGER\]\[5\]')
400+
self.assertRegexpMatches (lines[1], '\[STRING\]\[A String\]')
401+
402+
344403
if __name__ == '__main__':
345404
# let the tests find the parent irods lib
346405
sys.path.insert(0, os.path.abspath('../..'))

0 commit comments

Comments
 (0)