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 Jinja2 #191

Closed
wants to merge 11 commits into from
Closed

update Jinja2 #191

wants to merge 11 commits into from

Conversation

lonnen
Copy link
Contributor

@lonnen lonnen commented Apr 8, 2022

Markup, a unpinned dependency of Jinja2, decided to remove a deprecated function that Jinja relied on, causing Jinja2 to throw an error sometime in Feb 2020.

Jinja2 has upgraded and recommends affected users upgrade rather than pinning an old version of Markup.

This PR incorporates that update into sphinx-js

Markup, a unpinned dependency of Jinja2, decided to remove a deprecated function that Jinja relied, causing Jinja2 to throw an error sometime in Feb 2020.

Jinja2 has upgraded and recommends affected users upgrade rather than pinning an old version of Markup. This PR incorporates that update into sphinx-js
@lonnen
Copy link
Contributor Author

lonnen commented Apr 8, 2022

This is an improvement as is, but is insufficient to fix the current build issues. Sphinx also needs an update to >4.0, which fixes build issues but changes behavior such that ~10% of tests fail. I'm looking into that now.

CircleCI is running tests on the branch because it's off the main repo, but isn't reporting them on PRs. No idea why. I'll see if that's quick to fix at the same time

@lonnen
Copy link
Contributor Author

lonnen commented Apr 8, 2022

8 of the 9 new errors introduced by Sphinx >4.0 are changes in type annotations, which are now returning as "type()" instead of the expected *type*

There is one case of static not annotating a static methods definition as expected

sphinx relies on other deprecated Jinja functions that are now removed, such that in order to get a working build we have to update Sphinx to 4.x at the same time.

Note that this introduced behavioral changes that break our tests: `"type()"` is returned instead of `*type*` and `autofunction_static` no longer works. This at least installs deps and gets to the testrunner before it falls over, which is an improvement
@lonnen
Copy link
Contributor Author

lonnen commented Apr 9, 2022

Pushing a version with the Sphinx update before I put this down for a bit. autofunction_static is misbehaving still and I'm no closer to understanding the type annotation changes than I was before, so e1d25f4 will still fail tests

lonnen added 5 commits April 10, 2022 18:14
this enables running single tests using something like `tox -e pyXX -- tests/file.py::TestClass::test_method`
earlier versions of parsimonious relied on a deprecated api call that is removed in 3.10, and this update removes a warning at test time
this matches the style of other tests around it and improves the readability of the expected result
this matches the style of other tests around it and improves the readability of the expected result
…claration

previously we expected jsdoc to return the line number of the constructor when documenting a class, which was considered "not ideal".
@lonnen
Copy link
Contributor Author

lonnen commented Apr 14, 2022

The behavior around constructors has changed in the updated libraries. They are still artificially marked undocumented by JSDoc but they now have examples and other properties appropriately attached (see jsdoc/jsdoc#1129).

In jsdoc.py there is a bunch of special casing for recreating the constructor. There seem to be several issues where class and constructor meta information are mixed up this way, including the fix in 5bb6ad1

lonnen added 3 commits April 16, 2022 02:12
this changes the class and class constructor tests to match what should be expected from the test js file. JSDoc behavior changed and the brittle workarounds of prior old behavior need to be re-examined. This doesn't fix the build, but it does fix some failures while introducing others
sphinx-js assumes semver, but both Jinja2 and Sphinx have removed functionality in minor version bumps that broke this library. This pins specific versions instead of relying on major version ranges.
Sphinx 4.3 removed the simple display_prefix string and replaced it with a function that is expected to return a list of nodes, which can be created from a string. This implements that for static, and changes both the sample code and the test to expect a more descriptively named static function intended to make debugging and code tracing easier
@lonnen
Copy link
Contributor Author

lonnen commented Apr 24, 2022

Types are incorrectly being printed as "type()" instead of *type*. Unlike display_prefix, nothing is jumping out from the sphinx changelog.

This is difficult to trace with the VSCode python debugger because it has no apparent programmatic watch or ability to search through variables in the namespace and these documents get deep. I followed the fields renderer inward using the linkDensity function in test_build_js/source/code.js and it accurately identified the Node param as a type. The Jinja template is right and the :param Node node: directive is correct as far as I can follow it, but things get tedious to follow as the document tree is constructed.

On the other end directives.py:42 AutoFunctionRenderer returns a document with the node field nested 27 steps deep. At this point "type()" is listed wrong. I have still not identified exactly where :param Node node: is converted into the wrong string

This is the lowest version number of sphinx where any tests succeed. This will minimize the number of external changes to examine while trying to restore the test suite
@lonnen
Copy link
Contributor Author

lonnen commented Apr 25, 2022

I thought to bissect versions of Sphinx to determine where the breaking change was introduced, but it's difficult to even recreate a historically working version. The last known working version of Sphinx was 3.5.2, but it's not simple to wind back version numbers and get a working test run.

This latest commit pins Sphinx at 4.1.0, which is the first version that uses jinja2 3.0 and the first version where any of the tests pass. This should at least help narrow down the changes, and we can ignore things introduced in 4.2+.x (for now)

I've also confirmed that built-in sphinx js: directive is doing the work here, and that the type appears correctly formatted as :type Node: before being transformed incorrectly into the document tree

@lonnen
Copy link
Contributor Author

lonnen commented May 10, 2022

landing either #195 or #197 and rebasing onto the new main branch will help narrow the change set here. Pausing until we get that done

@lonnen
Copy link
Contributor Author

lonnen commented May 13, 2022

Ok, with the main branch working it was easier to isolate the breaking changes.

Sphinx 4.0.x requires downgrading MarkupSafe to <2.0 so I did that, built this with MarkupSafe 1.1.1 and {Sphinx 3.x.x, 4.0.x}. When I try to build with Sphinx 4.1.0 the test failures described above show up. This is one example:

tests/test_build_js/test_build_js.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <test_build_js.Tests testMethod=test_autofunction_callback>
filename = 'autofunction_callback'
contents = 'requestCallback(responseCode)\n\n   Some global callback\n\n   Arguments:\n      * **responseCode** (*number*) --\n'

    def _file_contents_eq(self, filename, contents):
>       assert self._file_contents(filename) == contents
E       AssertionError: assert ('requestCallback(responseCode)\n'\n '\n'\n '   Some global callback\n'\n '\n'\n '   Arguments:\n'\n '      * **responseCode** ("number()") --\n') == ('requestCallback(responseCode)\n'\n '\n'\n '   Some global callback\n'\n '\n'\n '   Arguments:\n'\n '      * **responseCode** (*number*) --\n')
E           requestCallback(responseCode)
E           
E              Some global callback
E           
E              Arguments:
E         -       * **responseCode** (*number*) --
E         ?                           ^      ^
E         +       * **responseCode** ("number()") --
E         ?                           ^      ^ ++

See: https://github.com/mozilla/sphinx-js/tree/190-update-jinja-take2

up next: WTF changed in Sphinx 4.1.0 and how do we work around it

@lonnen
Copy link
Contributor Author

lonnen commented May 15, 2022

The changelog was not useful so I bisected every change in Sphinx between 4.1.0 and 4.0.3 and narrowed it to:

sphinx-doc/sphinx@4534d2d

May 5, 2021   43434a086cc1967bf15197ca4c59efd63dec2d4a good
June 1, 2021  01918fc2d360be25d6a1238f17e1955994e1a52e good
June 3, 2021  01918fc2d360be25d6a1238f17e1955994e1a52e good
June 3, 2021  69cbf7aa92b2d30a1d865de7429f6388cb94b946 good
June 3, 2021  bc0e3b4405ebd8e16131766ad579e76a76370556 good
June 3, 2021  4776cd329c5c799a7595c1b79e95d2a2fa12f07b good
June 3, 2021  a02f6c5e0cb9d68ad3cd9ef66f16c025947f608e good
June 3, 2021  4534d2d1a5755c8cbc9ef4327eab7e34a85a7de8 fail
June 3, 2021  ec9d578ed35fd158a17cadb4852b989198204f91 fail
June 4, 2021  732a7d673d2af00e20c402780096a06f9881a735 fail
June 13, 2021 244dedb534090ea4a1ad6e946f806760b9813f84 fail

edit: I grabbed the wrong sha initially so I corrected that, added a complete record of my local tests

@lonnen
Copy link
Contributor Author

lonnen commented May 18, 2022

The change is part of PR 9155, which is a fix for Sphinx issue 8945.

@lonnen
Copy link
Contributor Author

lonnen commented May 27, 2022

I've been tracing tests/test_build_js/test_build_js.py::Tests::test_autofunction_explicit to learn more. Here are notes for future me:

VSCode's python debugger is fine but in order to investigate these failures the setting "justMyCode": false needs to be in the .vscode/launch.json file as part of a block of configuring for the unit test debugger. This will let the debugger investigate libraries downloaded by tox, which is necessary to make progress on these failures.

location.source is a useful watch value for determining when we're dealing with linkDensity(). Even when evaluating a specific test a lot of less interesting functions will show up first and you'll need to advance the debugger manually until you get into the failing part of the test suite. It helps to rename the type, as well, so that it stands out from the variable name when you're digging around the object tree. Here I've been using a local patch renaming it to NodeType.

E              Arguments:
E         -       * **node** (*NodeType*) -- Something of a single type
E         ?                   ^    ^
E         +       * **node** ("NodeType()") -- Something of a single type
E         ?                   ^    ^ ++        

When the /sphinx/directives/__init__.py::run:214 method is wrapping up the linkDensity() function of test_build_js/source/code.js

node.children[1]
	.children[1]
	.children[0]
	.children[1]
	.children[0]
	.children[0]
	.children[0]
	.children[2]
	.children[0]

is an instance of a <pending_xref: <literal...>> where the literal has a single child node <#text: 'NodeType()'>. This node's rawsource attribute is 'NodeType'. The parenthesis are added before this step. (It does not indicate an explanation for the replacement of asterisks * with double quotes " in the final result, though).

My next place to dig in is the Field.make_xref function. This is the most significantly modified unit of code in the commit that introduced these failures.

@lonnen
Copy link
Contributor Author

lonnen commented Jun 9, 2022

Still thinking about this but I'm unlikely to get much done here before July. Hopefully the breadcrumb notes I've left are more valuable than annoying if someone gets impatient and needs to run ahead of me.

I approved and merged #195 assuming that being able to narrow down the change would help me get a fix in quickly but that has not been the case. sphinx-doc/sphinx@4534d2d has a weird pending_xref special case for python that I didn't think was relevant to our JS case. However, in the debugger I see one in the doctree. It's the node inside the parenthesis that wrap the argument types (node: NodeType). This is so close to the problem area that it seems they must be related.

@lonnen
Copy link
Contributor Author

lonnen commented Sep 19, 2022

Some progress:

I previously determined that our unsolved errors shows up with the following sphinx commit: sphinx-doc/sphinx@4534d2d

I've locally created a new set of tests called test_minimal by cloning the test_build_js folder and stripping it down to a minimal reduced case:

from tests.testing import SphinxBuildTestCase


class Tests(SphinxBuildTestCase):
    """Tests which require our big JS Sphinx tree to be built.

    Yes, it's too coupled.

    Many of these are renderer tests, but some indirectly test JS analysis.
    These latter are left over from when JS was the only supported language and
    had its assumptions coded into the renderers.

    """
    def test_autofunction_minimal(self):
        """Make sure we render correctly and pull the params out of the JS code
        when only the function name is provided."""
        self._file_contents_eq(
            'autofunction_minimal',
            'linkDensity(node)' + DESCRIPTION + FIELDS)


DESCRIPTION = """

   Return the ratio of the inline text length of the links in an
   element to the inline text length of the entire element."""

FIELDS = u"""

   Arguments:
      * **node** (*Node*) -- Something of a single type

   Throws:
      **PartyError|FartyError** -- Something with multiple types and a
      line that wraps

   Returns:
      **Number** -- What a thing
"""

and the associated source directory with a minimal code.js:

/**
 * Return the ratio of the inline text length of the links in an element to
 * the inline text length of the entire element.
 *
 * @param {Node} node - Something of a single type
 * @throws {PartyError|FartyError} Something with multiple types and a line
 *    that wraps
 * @returns {Number} What a thing
 */
function linkDensity(node) {
    const length = node.flavors.get('paragraphish').inlineLength;
    const lengthWithoutLinks = inlineTextLength(node.element,
                                                element => element.tagName !== 'A');
    return (length - lengthWithoutLinks) / length;
}

with a few files in a minimized docs subfolder that is tucked away in there.

Using the specific sphinx commit and python3.9 I traced one whole run of the failing test, including library functions. Unfortunately that wasn't so useful. Even with watched variables it was difficult to track the relevant bits of state. Sphinx uses regular expressions and state machines to dynamically assemble and expand trees. Even line by line I couldn't follow how we ended up in a bad state.

I went back to looking at the specific changes in 4534d2d1a5755c8cbc9ef4327eab7e34a85a7de8 and set breakpoints around the changes in both files. The domains/python.py file is not accessed in a failing test run, but utilities/docfields.py::make_xref is executed three times:

  1. node with no rolename and no contnode such that it calls the default innernode() and returns the resultant literalstrong textnode
  2. Node with rolename func which is turned into [pending_xref: <literal: <#text: 'Node()'>>]
  3. PartyError|FartyError with rolename func which is turned into [<pending_xref: <literal: <#text: 'PartyError|FartyError()'>>>]

Items 2 and 3 from that list correspond to the two differences in the actual string (compared to the expected string) resulting in our test failure. List item 1 appears to be the variable name which precedes the type in the docstring, and also appears to be correctly represented.

If list items 2 and 3 had no rolename, instead of func, they would probably also come through as literalstrong and render appropriately.

Next steps:

  1. confirm this with similar breakpoints in a known working version of sphinx
  2. trace down where the rolename is being assigned

The sphinx change associated with our problem added some additional logic here that should only have applied for python classes. Its possible this was always a benign bit of extra state, and even though we're in the JS domain we're being caught by this python-specific code in a general utility class unexpectedly

@lonnen
Copy link
Contributor Author

lonnen commented Oct 10, 2022

The previous version of sphinx indeed ignored the role and had no inliner:

refnode = addnodes.pending_xref('', refdomain=domain, refexplicit=False, reftype=rolename, reftarget=target)
refnode += contnode or innernode(target, target)
if env:
    env.get_domain(domain).process_field_xref(refnode)
return refnode

When I revert just the body of the docfields.py:make_xref function everything passes.

In the new code an inliner is passed to make_xref and a role ('func') is retrieved from the domain ('js'). If there is no inliner, we run the original path (above). If there is no role, a warning is logged and then we run to the original code path.

The inliner is part of the new code. It's originally retrieved from the directive ('js:function') in docfields.py:transform and passed through make_field, handle_item, make_xrefs, to make_xref.

It's not immediately obvious to me what to do with this from sphinx-js code

@lonnen
Copy link
Contributor Author

lonnen commented Dec 10, 2022

Jinja2 has been updated

This is mostly useful for context on some other issues, but it is linked directly so I'm not worried about discoverability. It can be closed for now.

@lonnen lonnen closed this Dec 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant