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

Update some python deps for python>=3.8 compat #251

Merged
merged 1 commit into from
Aug 26, 2022

Conversation

yohanboniface
Copy link
Contributor

Trying to run tests, I've hit some bugs related to my local python version (3.10).

The issue is related to ABC classes moved from collections to collections.abc since
python 3.3, and retro compat has ended with 3.8

I suggest to upgrade those two packages html5lib and sortedcontainers.

It's gonna be harder for the other package hitting the same issue: messytables. This package seems prehistoric (like I am, I've worked too with OKFN long time ago…), it seems no more maintained (messytables is deprecated in favour of tabular-py, which itself seems deprecated in favor of the Frictionless Framework). The package has a lot of old dependencies, including nosetests, which itself is unmaintained.

I can work on clearing this situation, either by updating the Grist code to move over from messytables or by trying to update messytables (hopefully then released), but I'll wait for your advices on this before ;)

Note: after updating those packages and patching messytables, I still have 17 failing tests. Those seems unrelated to my python version, but I may miss something.

@alexmojaki
Copy link
Contributor

alexmojaki commented Aug 21, 2022

Thanks for the PR @yohanboniface. Here are some assorted thoughts:

  • We definitely want to upgrade to 3.10 in the not-too-distant-future, so that users can use the latest syntax and library functions in their documents.
  • It looks like python_dateutil also needs to be upgraded for similar reasons, although that creates new test failures.
  • messytables is indeed a problem, just in general. We started to move away from it recently (6c90de4), it would be good to finish that process.
  • I see a few test failures caused by small differences between 3.9 and 3.10, but not major problems. However I don't think we want to constantly run tests in both versions (maintaining Python 2 support is bad enough) so I don't know about having branches in the tests for the two versions.
  • In any case, upgrading these packages is probably a good idea even if it doesn't get us all the way to a 3.10 upgrade, although I haven't actually looked into these packages and their changelogs so I can't confirm yet that it's safe.

@yohanboniface
Copy link
Contributor Author

It looks like python_dateutil also needs to be upgraded for similar reasons, although that creates new test failures.

OK, I can have a look :)

messytables is indeed a problem, just in general. We started to move away from it recently (6c90de4), it would be good to finish that process.

Ah, great! Can I help in some way ?

maintaining Python 2 support is bad enough)

Just to know, why still supporting python 2 ?

@alexmojaki
Copy link
Contributor

It looks like python_dateutil also needs to be upgraded for similar reasons, although that creates new test failures.

OK, I can have a look :)

Do you want to do that as part of this PR?

messytables is indeed a problem, just in general. We started to move away from it recently (6c90de4), it would be good to finish that process.

Ah, great! Can I help in some way ?

If you are willing, you could try changing the implementation of import_csv.py to not use messytables at all, while ensuring that all the tests still pass in Python 3.9. That would be greatly appreciated. But that would be separate from this PR.

You may want to use a new library to do the work that messytables was doing, in which case you can choose a library which you think is best. Just share your choice before you start working with it. We don't want to add too many requirements, and the licenses need to be compatible.

Just to know, why still supporting python 2 ?

Python 3 is still fairly new in Grist, so many documents still run on Python 2 and use formulas that only work in Python 2 and can't be updated automatically.

The good news is that this doesn't affect imports, even for documents using Python 2. So importing code and tests only needs to work in Python 3.

@yohanboniface
Copy link
Contributor Author

Do you want to do that as part of this PR?

I'd say so :)

Is there a way to rerun only the failing tests ? (I'm running tests with yarn test currently)

@alexmojaki
Copy link
Contributor

You can set the environment variable GREP_TESTS to only run tests where the name of the suite/test contains that string.

Are you getting JS test failures with the changes here? Did all the Python tests pass?

@yohanboniface
Copy link
Contributor Author

You can set the environment variable GREP_TESTS to only run tests where the name of the suite/test contains that string.

Ah thanks for the tip :)

Are you getting JS test failures with the changes here?

Well, I do have some failure anyway locally, and given the number of tests and the time it takes to run the full suite, it's a bit hard to easily know if there are new tests failing. This is why I wanted to only run the failing tests ;)

Did all the Python tests pass?

Ah! I didn't notice the python tests. Are you talking about the ŧest_xxx inside the sandbox/grist folder ? Are they run when running yarn test or is there a specific command to run ?

Maybe I've missed contribution guide somewhere ? :)

@yohanboniface
Copy link
Contributor Author

Are they run when running yarn test or is there a specific command to run ?

This seems to do the trick: PYTHONPATH=sandbox_venv3/lib/python3.10/site-packages/ py.test sandbox/grist/

There are a bunch of failure I'll investigate!

@yohanboniface
Copy link
Contributor Author

When running in a python 3.6 venv, I have one error failing, this one:

sandbox/grist/test_engine.py:275: in assertFormulaError
    self.assertRegex(exc.details, tracebackRegexp)
E   AssertionError: Regex didn't match: '\n  File "usercode", line 2\n    if sum\\(3, 5\\) > 6:\nIndentationError: unexpected indent\n' not found in 'Traceback (most recent call last):\n  File "/home/ybon/Code/ts/grist-core/sandbox/grist/engine.py", line 954, in _recompute_one_cell\n    result = col.method(record, table.user_table)\n  File "usercode", line 262, in indent_err\n    raise IndentationError(\'unexpected indent\\n\\nAn `IndentationError` occurs when a given line of code is\\nnot indented (aligned vertically with other lines) as expected.\\n\\nLine `2` identified above is more indented than expected.\\n\\n\', (\'usercode\', 2, 2, \'  if sum(3, 5) > 6:\'))\n  File "usercode", line 2\n    if sum(3, 5) > 6:\n    ^\nIndentationError: unexpected indent\n\nAn `IndentationError` occurs when a given line of code is\nnot indented (aligned vertically with other lines) as expected.\n\nLine `2` identified above is more indented than expected.\n\n\n'

The regex does not seem up to date:

(Pdb) print(expected_regex.pattern)  

  File "usercode", line 2
    if sum\(3, 5\) > 6:
IndentationError: unexpected indent

(Pdb) print(text)
Traceback (most recent call last):
  File "/home/ybon/Code/ts/grist-core/sandbox/grist/engine.py", line 954, in _recompute_one_cell
    result = col.method(record, table.user_table)
  File "usercode", line 262, in indent_err
    raise IndentationError('unexpected indent\n\nAn `IndentationError` occurs when a given line of code is\nnot indented (aligned vertically with other lines) as expected.\n\nLine `2` identified above is more indented than expected.\n\n', ('usercode', 2, 2, '  if sum(3, 5) > 6:'))
  File "usercode", line 2
    if sum(3, 5) > 6:
    ^
IndentationError: unexpected indent

An `IndentationError` occurs when a given line of code is
not indented (aligned vertically with other lines) as expected.

Line `2` identified above is more indented than expected.

Which python 3 version is the "standard" to run test without failure ? So I can use it as reference :)

@alexmojaki
Copy link
Contributor

Ah! I didn't notice the python tests. Are you talking about the test_xxx inside the sandbox/grist folder ? Are they run when running yarn test or is there a specific command to run ?

python sandbox/grist/runtests.py

(Ignore the docstring in that file mentioning nacl)

Or you can do what that script does more directly:

python -m unittest discover -s sandbox/grist

Maybe I've missed contribution guide somewhere ? :)

No, sorry, you haven't missed anything, this is our fault for not making more things public.

This seems to do the trick: PYTHONPATH=sandbox_venv3/lib/python3.10/site-packages/ py.test sandbox/grist/

pytest almost works, but not quite. There's one function test_undo that isn't a test but pytest thinks it is, which results in an error. pytest also doesn't notice a bunch of doctests which are specifically configured for unittest.

Also the way to use a venv is to first run source sandbox_venv3/bin/activate and then the shell will be temporarily configured so that the python command will use the correct version and library paths.

Which python 3 version is the "standard" to run test without failure ? So I can use it as reference :)

Python 3.9. The patch version doesn't seem to matter - I'm running 3.9.2 locally, but if you put import sys; sys.version in a formula in https://docs.getgrist.com/ it says 3.9.13.

ABC classes have moved from collections to collections.abc since
python 3.3, and retro compat has ended with 3.8
@yohanboniface
Copy link
Contributor Author

Small journey in Grist tests :)


Running with python 3.9.13 in branch main (/sandbox_venv39/bin/python sandbox/grist/runtests.py): everything works, and we have those warnings:

/home/ybon/Code/ts/grist-core/sandbox/grist/functions/test_schedule.py:102: DeprecationWarning: Please use assertRaisesRegex instead.

/home/ybon/Code/ts/grist-core/sandbox_venv39/lib/python3.9/site-packages/dateutil/parser.py:587: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.10 it will stop working

/home/ybon/Code/ts/grist-core/sandbox/grist/test_acl_formula.py:108: DeprecationWarning: Please use assertRaisesRegex instead.

Running with python 3.10.6:

FAILED (errors=49)

  File "/home/ybon/Code/ts/grist-core/sandbox_venv3/lib/python3.10/site-packages/sortedcontainers/sortedlist.py", line 9, in <module>
    from collections import Sequence, MutableSequence
ImportError: cannot import name 'Sequence' from 'collections' (/usr/lib/python3.10/collections/__init__.py)

Upgrading sortedcontainers to 2.4.0:

FAILED (failures=17, errors=5, skipped=2)

      File "/home/ybon/Code/ts/grist-core/sandbox_venv3/lib/python3.10/site-packages/dateutil/parser.py", line 587, in parse
        if (isinstance(tzinfos, collections.Callable) or
    AttributeError: module 'collections' has no attribute 'Callable'

Upgrading python-dateutil to 2.8.2

FAILED (failures=5, errors=3, skipped=2)

  File "/home/ybon/Code/ts/grist-core/sandbox_venv3/lib/python3.10/site-packages/messytables/core.py", line 2, in <module>
    from collections import Mapping
ImportError: cannot import name 'Mapping' from 'collections' (/usr/lib/python3.10/collections/__init__.py)

Patching messytables:

FAILED (failures=5, errors=3, skipped=2)

  File "/home/ybon/Code/ts/grist-core/sandbox_venv3/lib/python3.10/site-packages/html5lib/_trie/_base.py", line 3, in <module>
    from collections import Mapping
ImportError: cannot import name 'Mapping' from 'collections' (/usr/lib/python3.10/collections/__init__.py)

Updating html5lib to 1.1:

No more errors, now let's look at failures:

FAILED (failures=5, skipped=2)

======================================================================
FAIL: test_suggest_column_type_methods (test_completion.TestCompletion)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ybon/Code/ts/grist-core/sandbox/grist/test_completion.py", line 282, in test_suggest_column_type_methods
    self.assertGreaterEqual(
AssertionError: {'$firstName.count(', '$firstName.rindex(', '$firstName.isalnum()', '$firstName.partition(', '$firstName.isascii()', '$firstName.casefold()', '$firstName.istitle()', '$firstName.rsplit(', '$firstName.rstrip(', '$firstName.translate(', '$firstName.isalpha()', '$firstName.title()', '$firstName.isidentifier()', '$firstName.lstrip(', '$firstName.rfind(', '$firstName.startswith(', '$firstName.isdigit()', '$firstName.find(', '$firstName.replace(', '$firstName.splitlines(', '$firstName.strip(', '$firstName.rpartition(', '$firstName.islower()', '$firstName.swapcase()', '$firstName.upper()', '$firstName.removeprefix(', '$firstName.expandtabs(', '$firstName.index(', '$firstName.ljust(', '$firstName.isdecimal()', '$firstName.split(', '$firstName.rjust(', '$firstName.center(', '$firstName.capitalize()', '$firstName.format(', '$firstName.isupper()', '$firstName.isnumeric()', '$firstName.isprintable()', '$firstName.zfill(', '$firstName.encode(', '$firstName.endswith(', '$firstName.isspace()', '$firstName.lower()', '$firstName.join(', '$firstName.format_map(', '$firstName.maketrans(', '$firstName.removesuffix('} not greater than or equal to {'$firstName.title(', '$firstName.replace(', '$firstName.startswith('}

======================================================================
FAIL: test_suggest_follows_references (test_completion.TestCompletion)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ybon/Code/ts/grist-core/sandbox/grist/test_completion.py", line 319, in test_suggest_follows_references
    self.assertGreaterEqual(
AssertionError: {'$school.yearFounded.real', '$school.yearFounded.from_bytes(', '$school.yearFounded.to_bytes(', '$school.yearFounded.as_integer_ratio()', '$school.yearFounded.bit_length()', '$school.yearFounded.numerator', '$school.yearFounded.conjugate(', '$school.yearFounded.imag', '$school.yearFounded.bit_count()', '$school.yearFounded.denominator'} not greater than or equal to {'$school.yearFounded.real', '$school.yearFounded.bit_length(', '$school.yearFounded.denominator'}

======================================================================
FAIL: test_value (test_completion.TestCompletion)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ybon/Code/ts/grist-core/sandbox/grist/test_completion.py", line 63, in test_value
    self.assertGreaterEqual(
AssertionError: {'value.isdigit()', 'value.rjust(', 'value.expandtabs(', 'value.find(', 'value.isupper()', 'value.capitalize()', 'value.rsplit(', 'value.isascii()', 'value.center(', 'value.maketrans(', 'value.isalpha()', 'value.startswith(', 'value.isnumeric()', 'value.format(', 'value.lower()', 'value.swapcase()', 'value.isidentifier()', 'value.removesuffix(', 'value.upper()', 'value.isprintable()', 'value.format_map(', 'value.encode(', 'value.lstrip(', 'value.isspace()', 'value.endswith(', 'value.isalnum()', 'value.strip(', 'value.zfill(', 'value.count(', 'value.istitle()', 'value.replace(', 'value.index(', 'value.casefold()', 'value.join(', 'value.rstrip(', 'value.translate(', 'value.isdecimal()', 'value.islower()', 'value.ljust(', 'value.title()', 'value.rindex(', 'value.partition(', 'value.split(', 'value.rpartition(', 'value.rfind(', 'value.splitlines(', 'value.removeprefix('} not greater than or equal to {'value.replace(', 'value.startswith(', 'value.title('}

======================================================================
FAIL: DATEVALUE (functions.date)
Doctest: functions.date.DATEVALUE
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python3.10/doctest.py", line 2221, in runTest
    raise self.failureException(self.format_failure(new.getvalue()))
AssertionError: Failed doctest test for functions.date.DATEVALUE
  File "/home/ybon/Code/ts/grist-core/sandbox/grist/functions/date.py", line 264, in DATEVALUE

----------------------------------------------------------------------
File "/home/ybon/Code/ts/grist-core/sandbox/grist/functions/date.py", line 292, in functions.date.DATEVALUE
Failed example:
    DATEVALUE("asdf")
Expected:
    Traceback (most recent call last):
    ...
    ValueError: Unknown string format
Got:
    Traceback (most recent call last):
      File "/usr/lib/python3.10/doctest.py", line 1350, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest functions.date.DATEVALUE[10]>", line 1, in <module>
        DATEVALUE("asdf")
      File "/home/ybon/Code/ts/grist-core/sandbox/grist/functions/date.py", line 297, in DATEVALUE
        return dateutil.parser.parse(date_string).replace(tzinfo=_get_tzinfo(tz))
      File "/home/ybon/Code/ts/grist-core/sandbox_venv3/lib/python3.10/site-packages/dateutil/parser/_parser.py", line 1368, in parse
        return DEFAULTPARSER.parse(timestr, **kwargs)
      File "/home/ybon/Code/ts/grist-core/sandbox_venv3/lib/python3.10/site-packages/dateutil/parser/_parser.py", line 643, in parse
        raise ParserError("Unknown string format: %s", timestr)
    dateutil.parser._parser.ParserError: Unknown string format: asdf


======================================================================
FAIL: test_make_module_text (test_gencode.TestGenCode)
Test that make_module_text produces the exact sample output that we have stored
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/ybon/Code/ts/grist-core/sandbox/grist/test_gencode.py", line 81, in test_make_module_text
    self.assertEqual(generated, saved_sample, "Generated code doesn't match sample:\n" +
AssertionError: 'import grist\nfrom functions import *   [1410 chars]))\n' != "import grist\nfrom functions import *   [1382 chars]))\n"
Diff is 2045 characters long. Set self.maxDiff to None to see it. : Generated code doesn't match sample:
--- generated
+++ usercode.py
@@ -48,4 +48,4 @@
   def badSyntax(rec, table):
     # for a in b
     # 10
-    raise SyntaxError("expected ':'\n\nA `SyntaxError` occurs when Python cannot understand your code.\n\nYou wrote a `for` loop but\nforgot to add a colon `:` at the end\n\n", ('usercode', 1, 11, 'for a in b'))
+    raise SyntaxError('invalid syntax\n\nA `SyntaxError` occurs when Python cannot understand your code.\n\nYou wrote a `for` loop but\nforgot to add a colon `:` at the end\n\n', ('usercode', 1, 11, 'for a in b'))

Looking at test_suggest_column_type_methods.

Test expects $firstName.title(, but it founds $firstName.title()

Lets try to adapt the expected string.

It fails again, but on another assertion: $yearFounded.bit_length() instead of $yearFounded.bit_length(.

Changing the expected string again.

Test passes.

Same for test_suggest_follows_references.

Will test later to rerun on python 3.9, but I'm expecting that those tests will
fail, and will need more work.


Looking at Doctest: functions.date.DATEVALUE.

Expected:
    Traceback (most recent call last):
    ...
    ValueError: Unknown string format
Got:
    Traceback (most recent call last):
    ...
    dateutil.parser._parser.ParserError: Unknown string format: asdf

Exception type changed since 2.8.1:

Parsing errors will now raise ParserError, a subclass of ValueError, which has a nicer string representation.

Not sure adapting the docstring will be enough, or anything would expect that
string to parse it before displaying to the user ?


Now looking at test_make_module_text:

--- generated
+++ usercode.py
@@ -48,4 +48,4 @@
   def badSyntax(rec, table):
     # for a in b
     # 10
-    raise SyntaxError("expected ':'\n\nA `SyntaxError` occurs when Python cannot understand your code.\n\nYou wrote a `for` loop but\nforgot to add a colon `:` at the end\n\n", ('usercode', 1, 11, 'for a in b'))
+    raise SyntaxError('invalid syntax\n\nA `SyntaxError` occurs when Python cannot understand your code.\n\nYou wrote a `for` loop but\nforgot to add a colon `:` at the end\n\n', ('usercode', 1, 11, 'for a in b'))

Trying to adpat the expected string.

Yay, all tests pass!


Let's try again in python 3.9, without surprise:

FAILED (failures=4, skipped=2)

Apart from the DATEVALUE test update, which is related to the upgrade of
python-dateutil, all other changes are related to the python upgrade itself.

On thing to notice, is that I only changed tests, not actual code.

So for now, I focus on updating libs and updating the test related to dateutil.

For the record, here is the diff I'm not including in this branch:

diff --git a/sandbox/grist/test_completion.py b/sandbox/grist/test_completion.py
index cf22235..2ee6c5f 100644
--- a/sandbox/grist/test_completion.py
+++ b/sandbox/grist/test_completion.py
@@ -62,7 +62,7 @@ class TestCompletion(test_engine.EngineTestCase):
     # Should have same type as column.
     self.assertGreaterEqual(
       set(self.engine.autocomplete("value.", "Schools", "lastModifier", self.user)),
-      {'value.startswith(', 'value.replace(', 'value.title('}
+      {'value.startswith(', 'value.replace(', 'value.title()'}
     )
     self.assertGreaterEqual(
       set(self.engine.autocomplete("value.", "Schools", "lastModified", self.user)),
@@ -281,7 +281,7 @@ class TestCompletion(test_engine.EngineTestCase):
     # Should treat columns as correct types.
     self.assertGreaterEqual(
       set(self.engine.autocomplete("$firstName.", "Students", "firstName", self.user)),
-      {'$firstName.startswith(', '$firstName.replace(', '$firstName.title('}
+      {'$firstName.startswith(', '$firstName.replace(', '$firstName.title()'}
     )
     self.assertGreaterEqual(
       set(self.engine.autocomplete("$birthDate.", "Students", "lastName", self.user)),
@@ -301,13 +301,13 @@ class TestCompletion(test_engine.EngineTestCase):
       set(self.engine.autocomplete("$yearFounded.", "Schools", "budget", self.user)),
       {
         '$yearFounded.denominator',    # Only integers have this
-        '$yearFounded.bit_length(',    # and this
+        '$yearFounded.bit_length()',    # and this
         '$yearFounded.real'
       }
     )
     self.assertGreaterEqual(
       set(self.engine.autocomplete("$budget.", "Schools", "budget", self.user)),
-      {'$budget.is_integer(', '$budget.real'}    # Only floats have this
+      {'$budget.is_integer()', '$budget.real'}    # Only floats have this
     )
 
   def test_suggest_follows_references(self):
@@ -320,7 +320,7 @@ class TestCompletion(test_engine.EngineTestCase):
       set(self.engine.autocomplete("$school.yearFounded.","Students", "firstName", self.user)),
       {
         '$school.yearFounded.denominator',
-        '$school.yearFounded.bit_length(',
+        '$school.yearFounded.bit_length()',
         '$school.yearFounded.real'
       }
     )
diff --git a/sandbox/grist/test_gencode.py b/sandbox/grist/test_gencode.py
index dff010c..bd7e3e3 100644
--- a/sandbox/grist/test_gencode.py
+++ b/sandbox/grist/test_gencode.py
@@ -73,10 +73,10 @@ class TestGenCode(unittest.TestCase):
     if six.PY3:
       saved_sample = saved_sample.replace(
         "raise SyntaxError('invalid syntax', ('usercode', 1, 11, u'for a in b'))",
-        "raise SyntaxError('invalid syntax\\n\\n"
-        "A `SyntaxError` occurs when Python cannot understand your code.\\n\\n"
-        "You wrote a `for` loop but\\nforgot to add a colon `:` at the end\\n\\n', "
-        "('usercode', 1, 11, 'for a in b'))"
+        """raise SyntaxError("expected ':'\\n\\n"""
+        """A `SyntaxError` occurs when Python cannot understand your code.\\n\\n"""
+        """You wrote a `for` loop but\\nforgot to add a colon `:` at the end\\n\\n", """
+        """('usercode', 1, 11, 'for a in b'))"""
       )
     self.assertEqual(generated, saved_sample, "Generated code doesn't match sample:\n" +
                      "".join(difflib.unified_diff(generated.splitlines(True),

@alexmojaki all the python 3.9 compatible stuff should be pushed, and should be enough for this PR. Not sure how to move forward, which means making those tests run in the same time on python 3.9 and 3.10, and dealing with messytable.

@alexmojaki
Copy link
Contributor

Thanks @yohanboniface, this is perfect. I'll fully review the pushed changes soon, we'll probably be able to merge them as is.

Actually upgrading to 3.10 will be more complicated and require some internal discussion, I don't know how soon it'll be possible to go forward with that.

@yohanboniface
Copy link
Contributor Author

Thanks @alexmojaki

Two questions/suggestions, not for this PR obviously:

  • what about running black on the python code ?
  • what about moving all the python tests in dedicated subfolder, just for easier browsing of both tests and business code ?

@alexmojaki
Copy link
Contributor

Everything looks good with the PR, thanks! I'll pass your questions on to the team.

@alexmojaki alexmojaki merged commit ec1d080 into gristlabs:main Aug 26, 2022
@yohanboniface yohanboniface deleted the py10 branch August 27, 2022 08:27
@vviers vviers added the anct label Jul 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants