From 871688f02617921452a77f50aba33fd8c5b4dbe5 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 24 Jul 2025 16:01:57 +0100
Subject: [PATCH] Update docs
---
.../IncorrectRaiseInSpecialMethod.py | 16 --------
.../IncorrectRaiseInSpecialMethod.qhelp | 40 +++++++++----------
.../IncorrectRaiseInSpecialMethod2.py | 15 -------
.../IncorrectRaiseInSpecialMethod3.py | 27 -------------
.../examples/IncorrectRaiseInSpecialMethod.py | 22 ++++++++++
.../IncorrectRaiseInSpecialMethod2.py | 7 ++++
.../IncorrectRaiseInSpecialMethod3.py | 4 ++
7 files changed, 52 insertions(+), 79 deletions(-)
delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py
delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py
delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py
create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py
create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py
create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py
deleted file mode 100644
index e76c27145db..00000000000
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py
+++ /dev/null
@@ -1,16 +0,0 @@
-#Incorrect unhashable class
-class MyMutableThing(object):
-
- def __init__(self):
- pass
-
- def __hash__(self):
- raise NotImplementedError("%r is unhashable" % self)
-
-#Make class unhashable in the standard way
-class MyCorrectMutableThing(object):
-
- def __init__(self):
- pass
-
- __hash__ = None
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
index f4f0cd6920a..a0c3463b9d1 100644
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
+++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp
@@ -9,7 +9,7 @@ When the expression a + b is evaluated the Python virtual machine w
is not implemented it will call type(b).__radd__(b, a).
Since the virtual machine calls these special methods for common expressions, users of the class will expect these operations to raise standard exceptions.
-For example, users would expect that the expression a.b might raise an AttributeError
+For example, users would expect that the expression a.b may raise an AttributeError
if the object a does not have an attribute b.
If a KeyError were raised instead,
then this would be unexpected and may break code that expected an AttributeError, but not a KeyError.
@@ -20,18 +20,18 @@ Therefore, if a method is unable to perform the expected operation then its resp
- - Attribute access,
a.b: Raise AttributeError
- - Arithmetic operations,
a + b: Do not raise an exception, return NotImplemented instead.
- - Indexing,
a[b]: Raise KeyError.
- - Hashing,
hash(a): Use __hash__ = None to indicate that an object is unhashable.
- - Equality methods,
a != b: Never raise an exception, always return True or False.
- - Ordering comparison methods,
a < b: Raise a TypeError if the objects cannot be ordered.
+ - Attribute access,
a.b (__getattr__): Raise AttributeError
+ - Arithmetic operations,
a + b (__add__): Do not raise an exception, return NotImplemented instead.
+ - Indexing,
a[b] (__getitem__): Raise KeyError or IndexError.
+ - Hashing,
hash(a) (__hash__): Should not raise an exception. Use __hash__ = None to indicate that an object is unhashable rather than raising an exception.
+ - Equality methods,
a == b (__eq__): Never raise an exception, always return True or False.
+ - Ordering comparison methods,
a < b (__lt__): Raise a TypeError if the objects cannot be ordered.
- Most others: Ideally, do not implement the method at all, otherwise raise
TypeError to indicate that the operation is unsupported.
-If the method is meant to be abstract, then declare it so using the @abstractmethod decorator.
+
If the method is intended to be abstract, then declare it so using the @abstractmethod decorator.
Otherwise, either remove the method or ensure that the method raises an exception of the correct type.
@@ -39,31 +39,29 @@ Otherwise, either remove the method or ensure that the method raises an exceptio
-This example shows two unhashable classes. The first class is unhashable in a non-standard way which may cause maintenance problems.
-The second, corrected, class uses the standard idiom for unhashable classes.
+In the following example, the __add__ method of A raises a TypeError if other is of the wrong type.
+However, it should return NotImplemented instead of rising an exception, to allow other classes to support adding to A.
+This is demonstrated in the class B.
-
+
-In this example, the first class is implicitly abstract; the __add__ method is unimplemented,
-presumably with the expectation that it will be implemented by sub-classes.
-The second class makes this explicit with an @abstractmethod decoration on the unimplemented __add__ method.
+In the following example, the __getitem__ method of C raises a ValueError, rather than a KeyError or IndexError as expected.
-
+
-In this last example, the first class implements a collection backed by the file store.
-However, should an IOError be raised in the __getitem__ it will propagate to the caller.
-The second class handles any IOError by reraising a KeyError which is the standard exception for
-the __getitem__ method.
+In the following example, the class __hash__ method of D raises TypeError.
+This causes D to be incorrectly identified as hashable by isinstance(obj, collections.abc.Hashable); so the correct
+way to make a class unhashable is to set __hash__ = None.
-
+
Python Language Reference: Special Method Names.
-Python Library Reference: Exceptions.
+Python Library Reference: Exceptions.
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py
deleted file mode 100644
index 405400bfe61..00000000000
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py
+++ /dev/null
@@ -1,15 +0,0 @@
-
-#Abstract base class, but don't declare it.
-class ImplicitAbstractClass(object):
-
- def __add__(self, other):
- raise NotImplementedError()
-
-#Make abstractness explicit.
-class ExplicitAbstractClass:
- __metaclass__ = ABCMeta
-
- @abstractmethod
- def __add__(self, other):
- raise NotImplementedError()
-
diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py
deleted file mode 100644
index 048d5043b4d..00000000000
--- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py
+++ /dev/null
@@ -1,27 +0,0 @@
-
-#Incorrect file-backed table
-class FileBackedTable(object):
-
- def __getitem__(self, key):
- if key not in self.index:
- raise IOError("Key '%s' not in table" % key)
- else:
- #May raise an IOError
- return self.backing.get_row(key)
-
-#Correct by transforming exception
-class ObjectLikeFileBackedTable(object):
-
- def get_from_key(self, key):
- if key not in self.index:
- raise IOError("Key '%s' not in table" % key)
- else:
- #May raise an IOError
- return self.backing.get_row(key)
-
- def __getitem__(self, key):
- try:
- return self.get_from_key(key)
- except IOError:
- raise KeyError(key)
-
diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py
new file mode 100644
index 00000000000..77c623bef79
--- /dev/null
+++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py
@@ -0,0 +1,22 @@
+class A:
+ def __init__(self, a):
+ self.a = a
+
+ def __add__(self, other):
+ # BAD: Should return NotImplemented instead of raising
+ if not isinstance(other,A):
+ raise TypeError(f"Cannot add A to {other.__type__}")
+ return A(self.a + other.a)
+
+class B:
+ def __init__(self, a):
+ self.a = a
+
+ def __add__(self, other):
+ # GOOD: Returning NotImplemented allows for other classes to support adding do B.
+ if not isinstance(other,B):
+ return NotImplemented
+ return B(self.a + other.a)
+
+
+
diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py
new file mode 100644
index 00000000000..ba5f90f4670
--- /dev/null
+++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py
@@ -0,0 +1,7 @@
+class C:
+ def __getitem__(self, idx):
+ if self.idx < 0:
+ # BAD: Should raise a KeyError or IndexError instead.
+ raise ValueError("Invalid index")
+ return self.lookup(idx)
+
diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py
new file mode 100644
index 00000000000..84ce9d18d27
--- /dev/null
+++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py
@@ -0,0 +1,4 @@
+class D:
+ def __hash__(self):
+ # BAD: Use `__hash__ = None` instead.
+ raise NotImplementedError(f"{self.__type__} is unhashable.")
\ No newline at end of file