diff --git a/CHANGES.rst b/CHANGES.rst index 37083327..89a68018 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,24 @@ Changelog ========= +0.18.2 (not yet released) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Bug fixes and minor changes +--------------------------- + ++ `#83`_, `#84`_: enable ordering on one to many relationships in + class :class:`icat.query.Query`. + ++ `#84`_: Add warning classes + :exc:`icat.exception.QueryOneToManyOrderWarning` and + :exc:`icat.exception.QueryWarning`, the latter being a common base + class for warnings emitted during creation of a query. + +.. _#83: https://github.com/icatproject/python-icat/issues/83 +.. _#84: https://github.com/icatproject/python-icat/pull/84 + + 0.18.1 (2021-04-13) ~~~~~~~~~~~~~~~~~~~ diff --git a/doc/src/exception.rst b/doc/src/exception.rst index b36cb28a..1378cad1 100644 --- a/doc/src/exception.rst +++ b/doc/src/exception.rst @@ -103,10 +103,18 @@ Exceptions raised by python-icat :members: :show-inheritance: +.. autoexception:: icat.exception.QueryWarning + :members: + :show-inheritance: + .. autoexception:: icat.exception.QueryNullableOrderWarning :members: :show-inheritance: +.. autoexception:: icat.exception.QueryOneToManyOrderWarning + :members: + :show-inheritance: + .. autoexception:: icat.exception.ClientVersionWarning :members: :show-inheritance: @@ -178,7 +186,9 @@ The class hierarchy for the exceptions is:: +-- IDSResponseError +-- GenealogyError +-- Warning - +-- QueryNullableOrderWarning + +-- QueryWarning + | +-- QueryNullableOrderWarning + | +-- QueryOneToManyOrderWarning +-- ClientVersionWarning +-- DeprecationWarning +-- ICATDeprecationWarning diff --git a/icat/exception.py b/icat/exception.py index 876f2c65..df37e8be 100644 --- a/icat/exception.py +++ b/icat/exception.py @@ -26,7 +26,7 @@ # icat.config 'ConfigError', # icat.query - 'QueryNullableOrderWarning', + 'QueryWarning', 'QueryNullableOrderWarning', 'QueryOneToManyOrderWarning', # icat.client, icat.entity 'ClientVersionWarning', 'ICATDeprecationWarning', 'EntityTypeError', 'VersionMethodError', 'SearchResultError', @@ -305,14 +305,34 @@ class ConfigError(_BaseException): # ================ Exceptions raised in icat.query ================= -class QueryNullableOrderWarning(Warning): - """Warn about using a nullable relation for ordering. +class QueryWarning(Warning): + """Warning while building a query. + + .. versionadded:: 0.18.2 + """ + pass + +class QueryNullableOrderWarning(QueryWarning): + """Warn about using a nullable many to one relation for ordering. + + .. versionchanged:: 0.18.2 + Inherit from :exc:`QueryWarning`. """ def __init__(self, attr): - msg = ("ordering on a nullable relation implicitly " + msg = ("ordering on a nullable many to one relation implicitly " "adds a '%s IS NOT NULL' condition." % attr) super(QueryNullableOrderWarning, self).__init__(msg) +class QueryOneToManyOrderWarning(QueryWarning): + """Warn about using a one to many relation for ordering. + + .. versionadded:: 0.18.2 + """ + def __init__(self, attr): + msg = ("ordering on a one to many relation %s may surprisingly " + "affect the search result." % attr) + super().__init__(msg) + # ======== Exceptions raised in icat.client and icat.entity ======== diff --git a/icat/query.py b/icat/query.py index c2ad4d10..47bbf24e 100644 --- a/icat/query.py +++ b/icat/query.py @@ -262,8 +262,12 @@ def setOrder(self, order): name and an order direction, the latter being either "ASC" or "DESC" for ascending or descending order respectively. :type order: iterable or :class:`bool` - :raise ValueError: if `order` contains invalid attributes that - either do not exist or contain one to many relationships. + :raise ValueError: if any attribute in `order` is not valid. + + .. versionchanged:: 0.18.2 + allow one to many relationships in `order`. Emit a + :exc:`~icat.exception.QueryOneToManyOrderWarning` rather + then raising a :exc:`ValueError` in this case. """ if order is True: @@ -291,9 +295,9 @@ def setOrder(self, order): warn(QueryNullableOrderWarning(pattr), stacklevel=sl) elif attrInfo.relType == "MANY": - raise ValueError("Cannot use one to many relationship " - "in '%s' to order %s." - % (obj, self.entity.BeanName)) + sl = 3 if self._init else 2 + warn(QueryOneToManyOrderWarning(pattr), + stacklevel=sl) if rclass is None: # obj is an attribute, use it right away. diff --git a/tests/test_06_query.py b/tests/test_06_query.py index 08c378bb..43ca3754 100644 --- a/tests/test_06_query.py +++ b/tests/test_06_query.py @@ -6,6 +6,7 @@ from distutils.version import StrictVersion as Version import re import sys +import warnings import pytest import icat import icat.config @@ -316,6 +317,33 @@ def test_query_nullable_warning_suppressed(client, recwarn): res = client.search(query) assert len(res) == 44 +def test_query_order_one_to_many(client, recwarn): + """Sort on a related object in a one yo many relation. + This has been enabled in #84, but a warning is still emitted. + """ + recwarn.clear() + query = Query(client, "Investigation", + order=['investigationInstruments.instrument.fullName']) + w = recwarn.pop(icat.QueryOneToManyOrderWarning) + assert issubclass(w.category, icat.QueryOneToManyOrderWarning) + assert "investigationInstruments" in str(w.message) + print(str(query)) + res = client.search(query) + assert len(res) == 3 + +def test_query_order_suppress_warnings(client, recwarn): + """Suppress all QueryWarnings. + """ + recwarn.clear() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=icat.QueryWarning) + query = Query(client, "Investigation", + order=['investigationInstruments.instrument.fullName']) + assert len(recwarn.list) == 0 + print(str(query)) + res = client.search(query) + assert len(res) == 3 + def test_query_limit(client): """Add a LIMIT clause to the last example. """