Skip to content

Commit 0c9600e

Browse files
d-w-moorealanking
authored andcommitted
[#586] implement xml_mode() in a new irods.helpers module
1 parent 858bac8 commit 0c9600e

5 files changed

Lines changed: 134 additions & 5 deletions

File tree

README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,17 @@ QUASI_XML parser for the default one:
736736

737737
```
738738
from irods.message import (XML_Parser_Type, ET)
739-
ET( XML_Parser_Type.QUASI_XML, session.server_version )
739+
ET( XML_Parser_Type.QUASI_XML,
740+
server_version = session.server_version
741+
)
742+
```
743+
744+
The server_version parameter can be used independently, if desired, to change the
745+
current thread's choice of entities during QUASI_XML transactions with the server.
746+
(This is only a concern when interacting with servers before iRODS 4.2.9.)
747+
748+
```
749+
ET(server_version = (4,2,8))
740750
```
741751

742752
Two dedicated environment variables may also be used to customize the
@@ -759,8 +769,20 @@ particular server version.
759769

760770
Finally, note that these global defaults, once set, may be overridden on
761771
a per-thread basis using `ET(parser_type, server_version)`.
762-
We can also revert the current thread's XML parser back to the global
763-
default by calling `ET(None)`.
772+
773+
The current thread's XML parser can always be reverted to the global default by the
774+
explicit use of `ET(None)`. However, when frequently switching back and forth between
775+
parsers, it may be more convenient to use the `xml_mode` context manager:
776+
777+
```
778+
# ... Interactions with the server now use the default XML parser.
779+
780+
from irods.helpers import xml_mode
781+
with xml_mode('QUASI_XML'):
782+
# ... Interactions with the server, in the current thread, temporarily use QUASI_XML
783+
784+
# ... We have now returned to using the default XML parser.
785+
```
764786

765787
Rule Execution
766788
--------------

irods/helpers/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import contextlib
2+
import re
3+
from ..test.helpers import (home_collection,
4+
make_session as make_test_session)
5+
from irods.message import (ET, XML_Parser_Type)
6+
7+
__all__ = ['make_session', 'home_collection', 'xml_mode']
8+
9+
10+
def make_session(test_server_version = False, **kwargs):
11+
return make_test_session(test_server_version = test_server_version, **kwargs)
12+
13+
make_session.__doc__ = re.sub(r'(test_server_version\s*)=\s*\w+',r'\1 = False',make_test_session.__doc__)
14+
15+
16+
@contextlib.contextmanager
17+
def xml_mode(s):
18+
"""In a with-block, this context manager can temporarily change the client's choice of XML parser.
19+
20+
Example usages:
21+
with("QUASI_XML"):
22+
# ...
23+
with(XML_Parser_Type.QUASI_XML):
24+
# ..."""
25+
26+
try:
27+
if isinstance(s,str):
28+
ET(getattr(XML_Parser_Type,s)) # e.g. xml_mode("QUASI_XML")
29+
elif isinstance(s,XML_Parser_Type):
30+
ET(s) # e.g. xml_mode(XML_Parser_Type.QUASI_XML)
31+
else:
32+
msg = "xml_mode argument must be a string (e.g. 'QUASI_XML') or an XML_Parser_Type enum."
33+
raise ValueError(msg)
34+
yield
35+
finally:
36+
ET(None)
37+

irods/message/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ def __repr__(self):
6767
XML_Parser_Type.__members__ = {k:v for k,v in XML_Parser_Type.__dict__.items()
6868
if isinstance(v,XML_Parser_Type)}
6969

70-
PARSER_TYPE_STRINGS = {v:k for k,v in XML_Parser_Type.__members__.items() if v.value != 0}
70+
# This creates a mapping from the "valid" (nonzero) XML_Parser_Type enums -- those which represent the actual parser
71+
# choices -- to their corresponding names as strings (e.g. XML_Parser_Type.STANDARD_XML is mapped to 'STANDARD_XML'):
72+
PARSER_TYPE_STRINGS = {v:k for k,v in XML_Parser_Type.__members__.items() if v.value != 0}
7173

7274
# We maintain values on a per-thread basis of:
7375
# - the server version with which we're communicating
@@ -111,6 +113,9 @@ def default_XML_parser(get_module = False):
111113
d = _default_XML
112114
return d if not get_module else _XML_parsers[d]
113115

116+
def string_for_XML_parser(parser_enum):
117+
return PARSER_TYPE_STRINGS[parser_enum]
118+
114119
_XML_parsers = {
115120
XML_Parser_Type.STANDARD_XML : ET_xml,
116121
XML_Parser_Type.QUASI_XML : ET_quasi_xml,

irods/test/data_obj_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1861,6 +1861,57 @@ def test_access_through_resc_hierarchy__243(self):
18611861
finally:
18621862
s.resources.remove('parent')
18631863

1864+
@unittest.skipIf(set(os.environ.keys()) & {'PYTHON_IRODSCLIENT_CONFIG__CONNECTIONS__XML_PARSER_DEFAULT',
1865+
'PYTHON_IRODSCLIENT_CONFIGURATION_PATH', 'PYTHON_IRODSCLIENT_DEFAULT_XML'},
1866+
"skipping due to possible overwriting of test-apropos settings by a configuration file or environment setting")
1867+
def test_temporary_xml_mode_change_with_operation_as_proof__issue_586(self):
1868+
from irods.helpers import (xml_mode, home_collection)
1869+
sess = irods.test.helpers.make_session()
1870+
hc = home_collection(sess)
1871+
odd_name = '{hc}/\1'.format(**locals())
1872+
1873+
# Currently 'STANDARD_XML' is the default, and 'QUASI_XML' is a convenient alternative to use when
1874+
# object names are used which contain special characters (e.g. '\1') hostile to standard XML parsers.
1875+
default_xml_parser = 'STANDARD_XML'
1876+
1877+
from irods.message import (current_XML_parser, string_for_XML_parser)
1878+
active_xml_parser_for_thread = lambda : string_for_XML_parser(current_XML_parser())
1879+
1880+
self.assertEqual(active_xml_parser_for_thread(), default_xml_parser)
1881+
1882+
with xml_mode('QUASI_XML'):
1883+
sess.data_objects.create(odd_name)
1884+
1885+
# Test that the xml parser setting isn't permanently changed
1886+
self.assertEqual(active_xml_parser_for_thread(), default_xml_parser)
1887+
1888+
try:
1889+
if default_xml_parser == 'STANDARD_XML':
1890+
with self.assertRaises(xml.etree.ElementTree.ParseError):
1891+
sess.collections.get(hc).data_objects
1892+
finally:
1893+
with xml_mode('QUASI_XML'):
1894+
sess.data_objects.unlink(odd_name, force = True)
1895+
1896+
def test_temporary_xml_mode_changes_have_desired_thread_limited_effect__issue_586(self):
1897+
from irods.message import (current_XML_parser, string_for_XML_parser)
1898+
active_xml_parser_for_thread = lambda : string_for_XML_parser(current_XML_parser())
1899+
from concurrent.futures import ThreadPoolExecutor
1900+
from irods.helpers import xml_mode
1901+
original_xml_parser = active_xml_parser_for_thread()
1902+
other_xml_parser = list({'STANDARD_XML', 'QUASI_XML', 'SECURE_XML'} - {original_xml_parser})[0]
1903+
1904+
self.assertNotEqual(other_xml_parser, original_xml_parser)
1905+
1906+
with xml_mode(other_xml_parser):
1907+
# Test that this thread is the only one affected, and that in it we get 'QUASI_XML' when we call
1908+
# current_XML_parser(), i.e. the function used internally by ET() to retrieve the current parser module.
1909+
self.assertEqual(other_xml_parser, active_xml_parser_for_thread())
1910+
self.assertEqual(original_xml_parser, ThreadPoolExecutor(max_workers = 1).submit(active_xml_parser_for_thread).result())
1911+
1912+
self.assertEqual(active_xml_parser_for_thread(), original_xml_parser)
1913+
1914+
18641915
def test_register_with_xml_special_chars(self):
18651916
test_dir = helpers.irods_shared_tmp_dir()
18661917
loc_server = self.sess.host in ('localhost', socket.gethostname())

irods/test/helpers.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,20 @@ def recast(k):
152152
# Create a connection for test, based on ~/.irods environment by default.
153153

154154
def make_session(test_server_version = True, **kwargs):
155+
"""Connect to an iRODS server as determined by any client environment
156+
file present at a standard location, and by any keyword arguments given.
157+
158+
Arguments:
159+
160+
test_server_version: Of type bool; in the `irods.test.helpers` version of this
161+
function, defaults to True. A True value causes
162+
*iRODS_Server_Too_Recent* to be raised if the server
163+
connected to is more recent than the current Python iRODS
164+
client's advertised level of compatibility.
165+
166+
**kwargs: Keyword arguments. Fed directly to the iRODSSession
167+
constructor. """
168+
155169
try:
156170
env_file = kwargs.pop('irods_env_file')
157171
except KeyError:
@@ -160,7 +174,6 @@ def make_session(test_server_version = True, **kwargs):
160174
except KeyError:
161175
env_file = os.path.expanduser('~/.irods/irods_environment.json')
162176
session = iRODSSession( irods_env_file = env_file, **kwargs )
163-
164177
if test_server_version:
165178
connected_version = session.server_version[:3]
166179
advertised_version = IRODS_VERSION[:3]
@@ -173,6 +186,7 @@ def make_session(test_server_version = True, **kwargs):
173186

174187

175188
def home_collection(session):
189+
"""Return a string value for the given session's home collection."""
176190
return "/{0.zone}/home/{0.username}".format(session)
177191

178192

0 commit comments

Comments
 (0)