Repository: petl-developers/petl

Issue:
DeprecationWarning / TypeError: Seeding based on hashing is deprecated/removed
## Problem description

### What's happenning

When creating a dummytable, the warning is emitted:
> DeprecationWarning: Seeding based on hashing is deprecated since Python 3.9 and will be removed in a subsequent version. The only supported seed types are: None, int, float, str, bytes, and bytearray.

Furthermore, when using the same code on 3.11 or 3.12, I get this error instead:
```
self = <random.Random object at 0x564b245ce8f0>
a = datetime.datetime(2024, 3, 11, 1, 27, 10, 858705), version = 2

    def seed(self, a=None, version=2):
        """Initialize internal state from a seed.
    
        The only supported seed types are None, int, float,
        str, bytes, and bytearray.
    
        None or no argument seeds from current time or from an operating
        system specific randomness source if available.
    
        If *a* is an int, all bits are used.
    
        For version 2 (the default), all of the bits are used if *a* is a str,
        bytes, or bytearray.  For version 1 (provided for reproducing random
        sequences from older versions of Python), the algorithm for str and
        bytes generates a narrower range of seeds.
    
        """
    
        if version == 1 and isinstance(a, (str, bytes)):
            a = a.decode('latin-1') if isinstance(a, bytes) else a
            x = ord(a[0]) << 7 if a else 0
            for c in map(ord, a):
                x = ((1000003 * x) ^ c) & 0xFFFFFFFFFFFFFFFF
            x ^= len(a)
            a = -2 if x == -1 else x
    
        elif version == 2 and isinstance(a, (str, bytes, bytearray)):
            if isinstance(a, str):
                a = a.encode()
            a = int.from_bytes(a + _sha512(a).digest())
    
        elif not isinstance(a, (type(None), int, float, str, bytes, bytearray)):
>           raise TypeError('The only supported seed types are: None,\n'
                            'int, float, str, bytes, and bytearray.')
E           TypeError: The only supported seed types are: None,
E           int, float, str, bytes, and bytearray.

/opt/hostedtoolcache/Python/3.11.8/x64/lib/python3.11/random.py:160: TypeError
```

### Expected behavior

The dummytable should be created with no warning or error.

## Scenario for reprodution

### Reproducible test case

Please provide a minimal, reproducible code sample, a copy-pastable example if possible:

_I haven't been able to make one yet, but changing this in the installed copy of petl in my .venv folder resolves the warning._

### Version and installation information

Please provide the following:

- Value of ``petl.__version__``: **1.7.14**
- Version information for any third-party package dependencies that are relevant
- Version of Python interpreter: **3.10**
- Operating system (Linux/Windows/Mac): **Linux Pop!_OS 22.04 LTS**
- How petl was installed (e.g., "using pip into virtual environment", or "using conda"): **uv pip install .venv**

Also, if you think it might be relevant, please provide the output from ``pip freeze`` or
``conda env export`` depending on which was used to install petl.

### Additional context

Add any other context about the problem here.

Also, feel free to remove all sections and text that aren't relevant.


Comments:

Patch:
diff --git a/docs/changes.rst b/docs/changes.rst
index 3d83bfd..5ff55eb 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -1,6 +1,15 @@
 Changes
 =======
 
+Version 1.7.15
+--------------
+
+* Add unit tests for randomtable, dummytable, and their supporting functions and classes.
+  By :user:`bmos`, :issue:`657`.
+
+* Fix: DeprecationWarning: Seeding based on hashing is deprecated since Python 3.9 and will be removed in a subsequent version.
+  By :user:`bmos`, :issue:`657`.
+
 Version 1.7.14
 --------------
 
diff --git a/petl/util/random.py b/petl/util/random.py
index 94fa758..7485abf 100644
--- a/petl/util/random.py
+++ b/petl/util/random.py
@@ -1,15 +1,24 @@
 from __future__ import absolute_import, print_function, division
 
-
-import datetime
-import random
+import hashlib
+import random as pyrandom
 import time
 from collections import OrderedDict
 from functools import partial
+
 from petl.compat import xrange, text_type
+from petl.util.base import Table
 
 
-from petl.util.base import Table
+def randomseed():
+    """
+    Obtain the hex digest of a sha256 hash of the
+    current epoch time in nanoseconds.
+    """
+
+    time_ns = str(time.time()).encode()
+    hash_time = hashlib.sha256(time_ns).hexdigest()
+    return hash_time
 
 
 def randomtable(numflds=5, numrows=100, wait=0, seed=None):
@@ -36,9 +45,11 @@ def randomtable(numflds=5, numrows=100, wait=0, seed=None):
         | 0.026535969683863625 |   0.1988376506866485 |  0.6498844377795232 |
         +----------------------+----------------------+---------------------+
         ...
+        <BLANKLINE>
 
     Note that the data are generated on the fly and are not stored in memory,
     so this function can be used to simulate very large tables.
+    The only supported seed types are: None, int, float, str, bytes, and bytearray.
 
     """
 
@@ -46,27 +57,25 @@ def randomtable(numflds=5, numrows=100, wait=0, seed=None):
 
 
 class RandomTable(Table):
-
     def __init__(self, numflds=5, numrows=100, wait=0, seed=None):
         self.numflds = numflds
         self.numrows = numrows
         self.wait = wait
         if seed is None:
-            self.seed = datetime.datetime.now()
+            self.seed = randomseed()
         else:
             self.seed = seed
 
     def __iter__(self):
-
         nf = self.numflds
         nr = self.numrows
         seed = self.seed
 
         # N.B., we want this to be stable, i.e., same data each time
-        random.seed(seed)
+        pyrandom.seed(seed)
 
         # construct fields
-        flds = ['f%s' % n for n in range(nf)]
+        flds = ["f%s" % n for n in range(nf)]
         yield tuple(flds)
 
         # construct data rows
@@ -74,18 +83,22 @@ class RandomTable(Table):
             # artificial delay
             if self.wait:
                 time.sleep(self.wait)
-            yield tuple(random.random() for n in range(nf))
+            yield tuple(pyrandom.random() for n in range(nf))
 
     def reseed(self):
-        self.seed = datetime.datetime.now()
-
-
-def dummytable(numrows=100,
-               fields=(('foo', partial(random.randint, 0, 100)),
-                       ('bar', partial(random.choice, ('apples', 'pears',
-                                                       'bananas', 'oranges'))),
-                       ('baz', random.random)),
-               wait=0, seed=None):
+        self.seed = randomseed()
+
+
+def dummytable(
+        numrows=100,
+        fields=(
+                ('foo', partial(pyrandom.randint, 0, 100)),
+                ('bar', partial(pyrandom.choice, ('apples', 'pears', 'bananas', 'oranges'))),
+                ('baz', pyrandom.random),
+        ),
+        wait=0,
+        seed=None,
+):
     """
     Construct a table with dummy data. Use `numrows` to specify the number of
     rows. Set `wait` to a float greater than zero to simulate a delay on each
@@ -108,14 +121,13 @@ def dummytable(numrows=100,
         |   4 | 'apples' |  0.09369523986159245 |
         +-----+----------+----------------------+
         ...
+        <BLANKLINE>
 
-        >>> # customise fields
-        ... import random
+        >>> import random as pyrandom
         >>> from functools import partial
-        >>> fields = [('foo', random.random),
-        ...           ('bar', partial(random.randint, 0, 500)),
-        ...           ('baz', partial(random.choice,
-        ...                           ['chocolate', 'strawberry', 'vanilla']))]
+        >>> fields = [('foo', pyrandom.random),
+        ...           ('bar', partial(pyrandom.randint, 0, 500)),
+        ...           ('baz', partial(pyrandom.choice, ['chocolate', 'strawberry', 'vanilla']))]
         >>> table2 = etl.dummytable(100, fields=fields, seed=42)
         >>> table2
         +---------------------+-----+-------------+
@@ -132,12 +144,19 @@ def dummytable(numrows=100,
         |  0.4219218196852704 |  15 | 'chocolate' |
         +---------------------+-----+-------------+
         ...
+        <BLANKLINE>
+
+        >>> table3_1 = etl.dummytable(50)
+        >>> table3_2 = etl.dummytable(100)
+        >>> table3_1[5] == table3_2[5]
+        False
 
     Data generation functions can be specified via the `fields` keyword
     argument.
 
     Note that the data are generated on the fly and are not stored in memory,
     so this function can be used to simulate very large tables.
+    The only supported seed types are: None, int, float, str, bytes, and bytearray.
 
     """
 
@@ -145,7 +164,6 @@ def dummytable(numrows=100,
 
 
 class DummyTable(Table):
-
     def __init__(self, numrows=100, fields=None, wait=0, seed=None):
         self.numrows = numrows
         self.wait = wait
@@ -154,7 +172,7 @@ class DummyTable(Table):
         else:
             self.fields = OrderedDict(fields)
         if seed is None:
-            self.seed = datetime.datetime.now()
+            self.seed = randomseed()
         else:
             self.seed = seed
 
@@ -167,7 +185,7 @@ class DummyTable(Table):
         fields = self.fields.copy()
 
         # N.B., we want this to be stable, i.e., same data each time
-        random.seed(seed)
+        pyrandom.seed(seed)
 
         # construct header row
         hdr = tuple(text_type(f) for f in fields.keys())
@@ -181,4 +199,4 @@ class DummyTable(Table):
             yield tuple(fields[f]() for f in fields)
 
     def reseed(self):
-        self.seed = datetime.datetime.now()
+        self.seed = randomseed()


Test patch:
diff --git a/petl/test/util/test_random.py b/petl/test/util/test_random.py
new file mode 100644
index 0000000..7c4f420
--- /dev/null
+++ b/petl/test/util/test_random.py
@@ -0,0 +1,93 @@
+import random as pyrandom
+import time
+from functools import partial
+
+from petl.util.random import randomseed, randomtable, RandomTable, dummytable, DummyTable
+
+
+def test_randomseed():
+    """
+    Ensure that randomseed provides a non-empty string that changes.
+    """
+    seed_1 = randomseed()
+    time.sleep(1)
+    seed_2 = randomseed()
+
+    assert isinstance(seed_1, str)
+    assert seed_1 != ""
+    assert seed_1 != seed_2
+
+
+def test_randomtable():
+    """
+    Ensure that randomtable provides a table with the right number of rows and columns.
+    """
+    columns, rows = 3, 10
+    table = randomtable(columns, rows)
+
+    assert len(table[0]) == columns
+    assert len(table) == rows + 1
+
+
+def test_randomtable_class():
+    """
+    Ensure that RandomTable provides a table with the right number of rows and columns.
+    """
+    columns, rows = 4, 60
+    table = RandomTable(numflds=columns, numrows=rows)
+
+    assert len(table[0]) == columns
+    assert len(table) == rows + 1
+
+
+def test_dummytable_custom_fields():
+    """
+    Ensure that dummytable provides a table with the right number of rows
+    and that it accepts and uses custom column names provided.
+    """
+    columns = (
+        ('count', partial(pyrandom.randint, 0, 100)),
+        ('pet', partial(pyrandom.choice, ['dog', 'cat', 'cow', ])),
+        ('color', partial(pyrandom.choice, ['yellow', 'orange', 'brown'])),
+        ('value', pyrandom.random),
+    )
+    rows = 35
+
+    table = dummytable(numrows=rows, fields=columns)
+    assert table[0] == ('count', 'pet', 'color', 'value')
+    assert len(table) == rows + 1
+
+
+def test_dummytable_no_seed():
+    """
+    Ensure that dummytable provides a table with the right number of rows
+    and columns when not provided with a seed.
+    """
+    rows = 35
+
+    table = dummytable(numrows=rows)
+    assert len(table[0]) == 3
+    assert len(table) == rows + 1
+
+
+def test_dummytable_int_seed():
+    """
+    Ensure that dummytable provides a table with the right number of rows
+    and columns when provided with an integer as a seed.
+    """
+    rows = 35
+    seed = 42
+    table = dummytable(numrows=rows, seed=seed)
+    assert len(table[0]) == 3
+    assert len(table) == rows + 1
+
+
+def test_dummytable_class():
+    """
+    Ensure that DummyTable provides a table with the right number of rows
+    and columns.
+    """
+    rows = 70
+    table = DummyTable(numrows=rows)
+
+    assert len(table) == rows + 1