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

[master] Fix Python3.13 compatibility regarding urllib.parse module #66899

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

marmarek
Copy link
Contributor

What does this PR do?

Python 3.13 fixed handling relative paths in urllib.parse module. Specifically, relative file URL is now constructed as file:path instead of converting it to absolute file:///path. This breaks salt.utils.url.create which expects file:/// specifically. The mismatch results in for example changing salt://top.sls into salt://.sls and thus not finding the top file.

Fix this by handling both prefixes.

Relevant python change: python/cpython#85110

What issues does this PR fix or reference?

Fixes: #66898

Merge requirements satisfied?

[NOTICE] Bug fixes or features added to Salt require tests.

Commits signed with GPG?

Yes

Please review Salt's Contributing Guide for best practices, including the
PR Guidelines.

See GitHub's page on GPG signing for more information about signing commits with GPG.

Copy link
Contributor

@twangboy twangboy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please write a test for this

Copy link
Contributor

@dmurphy18 dmurphy18 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs tests written in pytest

@marmarek
Copy link
Contributor Author

Could you please write a test for this

The existing tests/unit/utils/test_url.py already covers this. And indeed, without my change, it fails already on Python 3.13rc2 (Fedora 41 beta) or Python 3.12.6 (Fedora 40). The change that broke did got backported to Python 3.12.6 (released about 2 weeks ago). I see you have Fedora 40 in CI already, but it seems to run use onedir (and Python 3.10.x inside) instead of Python from Fedora. Is there any CI job that tests on Python 3.12.x (or 3.13)?

@twangboy
Copy link
Contributor

We currently only support up to Python 3.10. There are dependency issues we need to get figured out across all the Operating systems we support before we can officially support anything newer.

@marmarek
Copy link
Contributor Author

In absence of newer Python to actually test the behavior there, do you want me to mimic its new behavior by patching urllib.parse.urlunparse then (I find it weird, but if you insist I can add it)? The code is already tested that it still works on Python 3.10. And once you will be adding support for Python 3.12, you will have one thing less to fix :)

BTW, does the CI report somewhere test coverage? From what I can see, coverage-related jobs got skipped...

algitbot pushed a commit to alpinelinux/aports that referenced this pull request Sep 19, 2024
algitbot pushed a commit to alpinelinux/aports that referenced this pull request Sep 19, 2024
Copy link

@slimlv slimlv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how many "If/return"s you going to insert ?
note that changes in python code was introduced in minor release 3.12.6

try to make smart bulletproof improvements , for example

url = salt.utils.data.decode(urlunparse(("file", "", "/" + path, "", query, "")))

@twangboy
Copy link
Contributor

I think, just a unit test to make sure that if the URL starts with file:/// it gets converted to 'salt://`

Comment on lines 47 to 53

query = f"saltenv={saltenv}" if saltenv else ""
url = salt.utils.data.decode(urlunparse(("file", "", path, "", query, "")))
return "salt://{}".format(url[len("file:///") :])
# urlunparse changed behavior in Python 3.13
if url.startswith("file:///"):
return "salt://{}".format(url[len("file:///") :])
return "salt://{}".format(url[len("file:") :])
Copy link
Contributor

@bdrx312 bdrx312 Sep 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This create function should not be doing path = path.replace("\\", /) and probably not

if salt.utils.platform.is_windows():
path = salt.utils.path.sanitize_win_path(path)

Instead it should use urllib.request.pathname2url or some other python builtin functionality.

Especially since the sanitize_win_path function is clearly miswritten:

def sanitize_win_path(winpath):
    """
    Remove illegal path characters for windows
    """
    intab = "<>:|?*"
    if isinstance(winpath, str):
        winpath = winpath.translate({ord(c): "_" for c in intab})
    elif isinstance(winpath, str):

Notice the duplicate isinstance(winpath, str) in the elif that will never occur.

I would prefer instead of assuming that the string starts with file: or file:/// we do a regex search replace like re.sub(r"^file:(?:///)?", "", url) Also use f string instead of format.

return f'salt://{re.sub(r"^file:(?:///)?", "", url)}'

Copy link

@slimlv slimlv Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return f'salt://{re.sub(r"^file:(?:/+)?", "", url)}'

Copy link
Contributor

@bdrx312 bdrx312 Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return f'salt://{re.sub(r"^file:(?:/+)?", "", url)}'

If you are going to do zero or more you don't + and ? nor parenthesis you just use *

return f'salt://{re.sub(r"^file:/*", "", url)}'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and if you want to do 0 to 3 / then it would be

return f'salt://{re.sub(r"^file:/{0,3}", "", url)}'

or

return f'salt://{re.sub(r"^file:/{,3}", "", url)}'

but I think the explicit 0 is more readable.

@marmarek
Copy link
Contributor Author

marmarek commented Oct 4, 2024

I think, just a unit test to make sure that if the URL starts with file:/// it gets converted to 'salt://`

Most tests in tests/unit/utils/test_url.py do that already, for example test_create_url. Those tests do fail when running on Python 3.13 (or Python 3.12.6), so this regression is covered already. Your CI will notice it as soon as you start testing on such Python version.
And BTW, the file:// prefix is added only in salt.utils.url.create(), just to be replaced with salt:// - it doesn't exist in the function call argument.

@@ -47,7 +47,7 @@ def create(path, saltenv=None):

query = f"saltenv={saltenv}" if saltenv else ""
url = salt.utils.data.decode(urlunparse(("file", "", path, "", query, "")))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend just not using schema when calling unparse/unsplit:

Suggested change
url = salt.utils.data.decode(urlunparse(("file", "", path, "", query, "")))
return f"salt://{salt.utils.data.decode(urlunsplit(("", "", path, query, "")))}"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just read through the source code of urlunparse/urlunsplit and it looks like there is not currently any special handling for the file scheme so I agree that this is probably a better solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, lets see how this works. Tests on Python 3.12.6 are green with this version, I'll let CI check older ones.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using urlunparse is okay, but I think using urlunsplit as @yangskyboxlabs is more ideal since it passes one less unused argument.

@jfindlay
Copy link
Contributor

jfindlay commented Oct 10, 2024

In my opinion, having written both the original salt.utils.url and its accompanying tests, I agree the existing tests already cover the code well enough because obviously I write the most amazing test cases. I don't think it is necessary to cover the case that upstream will again change the behavior of code that hasn't been modified in ~25 years.

It is a subtle breaking change for a use case they very likely didn't expect. Regexing file:/// for salt:// is a yolo move, whereas subclassing the urlparse code to add a salt proto would have been more proper. (Maybe the upstream code doesn't care because you already can sub your own proto?) If anything a rare bug in old code is an indictment of the poor usecase. Not that urlparse.urlunparse's api is user friendly (pass the url components in a fixed length anonymous list; give me a break). There are still many occurrences in the codebase that handcraft salt:// URLs anyway.

Ironically, many years ago when I worked on this, Tom said "I wish we had gone with salt:///".

Python 3.13 fixed handling relative paths in urllib.parse module.
Specifically, relative file URL is now constructed as file:path instead
of converting it to absolute file:///path. This breaks
salt.utils.url.create which expects file:/// specifically. The mismatch
results in for example changing salt://top.sls into salt://.sls and thus
not finding the top file.

Fix this by handling both prefixes.

Relevant python change: python/cpython#85110
Fixes: saltstack#66898
@dead10ck
Copy link
Contributor

I'd like to remind the maintainers that this bug completely and utterly breaks salt on many systems, especially because this "fix" has been backported to Python 3.12.

I understand that 3.10 is currently the officially supported version, but the reality is that a lot of folks are not, and never will, use the onedir package, so this is high impact regardless of official compatibility guarantees.

I understand the desire for tests with changes in the general case, but I think folks have to keep in mind that that should be a rule of thumb, not a hard requirement in every case no matter what the cost. Sometimes fixing high impact bugs, level of effort to benefit ratio, and potential risk for introducing new regressions in more strongly supported environments should weigh into the decision to block merging unless there are tests. It's been established that there is significant preexisting coverage on this code path, so blocking on adding more tests is not doing anyone any good.

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.

[TECH DEBT] Salt fails to load top.sls on Python3.13rc2
8 participants