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, "")))
return "salt://{}".format(url[len("file:///") :])
return f'salt://{re.sub(r"file:/*", "", url)}'
Copy link
Contributor

@bdrx312 bdrx312 Oct 7, 2024

Choose a reason for hiding this comment

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

The regex needs a begin anchor ^ so it doesn't replace file:/ somewhere in the middle of the url: f'salt://{re.sub(r"^file:/*", "", url)}'

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
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
5 participants