Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#586] implement xml_mode for temporary parser changes #589

Merged
merged 1 commit into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,17 @@ QUASI_XML parser for the default one:

```
from irods.message import (XML_Parser_Type, ET)
ET( XML_Parser_Type.QUASI_XML, session.server_version )
ET( XML_Parser_Type.QUASI_XML,
server_version = session.server_version
)
```

The server_version parameter can be used independently, if desired, to change the
current thread's choice of entities during QUASI_XML transactions with the server.
(This is only a concern when interacting with servers before iRODS 4.2.9.)

```
ET(server_version = (4,2,8))
```

Two dedicated environment variables may also be used to customize the
Expand All @@ -755,8 +765,20 @@ particular server version.

Finally, note that these global defaults, once set, may be overridden on
a per-thread basis using `ET(parser_type, server_version)`.
We can also revert the current thread's XML parser back to the global
default by calling `ET(None)`.

The current thread's XML parser can always be reverted to the global default by the
explicit use of `ET(None)`. However, when frequently switching back and forth between
parsers, it may be more convenient to use the `xml_mode` context manager:

```
# ... Interactions with the server now use the default XML parser.

from irods.helpers import xml_mode
with xml_mode('QUASI_XML'):
# ... Interactions with the server, in the current thread, temporarily use QUASI_XML

# ... We have now returned to using the default XML parser.
```

Rule Execution
--------------
Expand Down
37 changes: 37 additions & 0 deletions irods/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import contextlib
import re
from ..test.helpers import (home_collection,
make_session as make_test_session)
from irods.message import (ET, XML_Parser_Type)

__all__ = ['make_session', 'home_collection', 'xml_mode']


def make_session(test_server_version = False, **kwargs):
return make_test_session(test_server_version = test_server_version, **kwargs)

make_session.__doc__ = re.sub(r'(test_server_version\s*)=\s*\w+',r'\1 = False',make_test_session.__doc__)


@contextlib.contextmanager
def xml_mode(s):
"""In a with-block, this context manager can temporarily change the client's choice of XML parser.

Example usages:
with("QUASI_XML"):
# ...
with(XML_Parser_Type.QUASI_XML):
# ..."""

try:
if isinstance(s,str):
ET(getattr(XML_Parser_Type,s)) # e.g. xml_mode("QUASI_XML")
elif isinstance(s,XML_Parser_Type):
ET(s) # e.g. xml_mode(XML_Parser_Type.QUASI_XML)
else:
msg = "xml_mode argument must be a string (e.g. 'QUASI_XML') or an XML_Parser_Type enum."
raise ValueError(msg)
yield
finally:
ET(None)

7 changes: 6 additions & 1 deletion irods/message/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ def __repr__(self):
XML_Parser_Type.__members__ = {k:v for k,v in XML_Parser_Type.__dict__.items()
if isinstance(v,XML_Parser_Type)}

PARSER_TYPE_STRINGS = {v:k for k,v in XML_Parser_Type.__members__.items() if v.value != 0}
# This creates a mapping from the "valid" (nonzero) XML_Parser_Type enums -- those which represent the actual parser
# choices -- to their corresponding names as strings (e.g. XML_Parser_Type.STANDARD_XML is mapped to 'STANDARD_XML'):
PARSER_TYPE_STRINGS = {v:k for k,v in XML_Parser_Type.__members__.items() if v.value != 0}
d-w-moore marked this conversation as resolved.
Show resolved Hide resolved

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

def string_for_XML_parser(parser_enum):
return PARSER_TYPE_STRINGS[parser_enum]

_XML_parsers = {
XML_Parser_Type.STANDARD_XML : ET_xml,
XML_Parser_Type.QUASI_XML : ET_quasi_xml,
Expand Down
51 changes: 51 additions & 0 deletions irods/test/data_obj_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1860,6 +1860,57 @@ def test_access_through_resc_hierarchy__243(self):
finally:
s.resources.remove('parent')

@unittest.skipIf(set(os.environ.keys()) & {'PYTHON_IRODSCLIENT_CONFIG__CONNECTIONS__XML_PARSER_DEFAULT',
'PYTHON_IRODSCLIENT_CONFIGURATION_PATH', 'PYTHON_IRODSCLIENT_DEFAULT_XML'},
"skipping due to possible overwriting of test-apropos settings by a configuration file or environment setting")
alanking marked this conversation as resolved.
Show resolved Hide resolved
def test_temporary_xml_mode_change_with_operation_as_proof__issue_586(self):
from irods.helpers import (xml_mode, home_collection)
sess = irods.test.helpers.make_session()
hc = home_collection(sess)
odd_name = '{hc}/\1'.format(**locals())

# Currently 'STANDARD_XML' is the default, and 'QUASI_XML' is a convenient alternative to use when
# object names are used which contain special characters (e.g. '\1') hostile to standard XML parsers.
default_xml_parser = 'STANDARD_XML'

from irods.message import (current_XML_parser, string_for_XML_parser)
active_xml_parser_for_thread = lambda : string_for_XML_parser(current_XML_parser())

self.assertEqual(active_xml_parser_for_thread(), default_xml_parser)

with xml_mode('QUASI_XML'):
sess.data_objects.create(odd_name)

# Test that the xml parser setting isn't permanently changed
self.assertEqual(active_xml_parser_for_thread(), default_xml_parser)

try:
if default_xml_parser == 'STANDARD_XML':
with self.assertRaises(xml.etree.ElementTree.ParseError):
sess.collections.get(hc).data_objects
finally:
with xml_mode('QUASI_XML'):
sess.data_objects.unlink(odd_name, force = True)

def test_temporary_xml_mode_changes_have_desired_thread_limited_effect__issue_586(self):
from irods.message import (current_XML_parser, string_for_XML_parser)
active_xml_parser_for_thread = lambda : string_for_XML_parser(current_XML_parser())
from concurrent.futures import ThreadPoolExecutor
from irods.helpers import xml_mode
original_xml_parser = active_xml_parser_for_thread()
other_xml_parser = list({'STANDARD_XML', 'QUASI_XML', 'SECURE_XML'} - {original_xml_parser})[0]

self.assertNotEqual(other_xml_parser, original_xml_parser)

with xml_mode(other_xml_parser):
# Test that this thread is the only one affected, and that in it we get 'QUASI_XML' when we call
# current_XML_parser(), i.e. the function used internally by ET() to retrieve the current parser module.
self.assertEqual(other_xml_parser, active_xml_parser_for_thread())
self.assertEqual(original_xml_parser, ThreadPoolExecutor(max_workers = 1).submit(active_xml_parser_for_thread).result())

self.assertEqual(active_xml_parser_for_thread(), original_xml_parser)


def test_register_with_xml_special_chars(self):
test_dir = helpers.irods_shared_tmp_dir()
loc_server = self.sess.host in ('localhost', socket.gethostname())
Expand Down
16 changes: 15 additions & 1 deletion irods/test/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,20 @@ def recast(k):
# Create a connection for test, based on ~/.irods environment by default.

def make_session(test_server_version = True, **kwargs):
"""Connect to an iRODS server as determined by any client environment
file present at a standard location, and by any keyword arguments given.

Arguments:

test_server_version: Of type bool; in the `irods.test.helpers` version of this
function, defaults to True. A True value causes
*iRODS_Server_Too_Recent* to be raised if the server
connected to is more recent than the current Python iRODS
client's advertised level of compatibility.

**kwargs: Keyword arguments. Fed directly to the iRODSSession
constructor. """

try:
env_file = kwargs.pop('irods_env_file')
except KeyError:
Expand All @@ -159,7 +173,6 @@ def make_session(test_server_version = True, **kwargs):
except KeyError:
env_file = os.path.expanduser('~/.irods/irods_environment.json')
session = iRODSSession( irods_env_file = env_file, **kwargs )

if test_server_version:
connected_version = session.server_version[:3]
advertised_version = IRODS_VERSION[:3]
Expand All @@ -172,6 +185,7 @@ def make_session(test_server_version = True, **kwargs):


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


Expand Down