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

Python 3.13 compatibility #470

Open
offa opened this issue Jan 16, 2025 · 6 comments · Fixed by #473 · May be fixed by #474
Open

Python 3.13 compatibility #470

offa opened this issue Jan 16, 2025 · 6 comments · Fixed by #473 · May be fixed by #474
Labels
Help Wanted We will be glad if somebody proposes a solution via PR

Comments

@offa
Copy link
Contributor

offa commented Jan 16, 2025

With #463 the library works on Python 3.12 (#430), but not on Python 3.13 yet:

AttributeError: module 'pathlib' has no attribute 'posixpath'. Did you mean: 'PosixPath'?

Reported location

Tested using master at 46728f6.

@allburov allburov added the Help Wanted We will be glad if somebody proposes a solution via PR label Jan 17, 2025
@offa
Copy link
Contributor Author

offa commented Jan 31, 2025

While the posixpath issue is fixed very easily, the issues that follow are more complicated.

--- a/artifactory.py
+++ b/artifactory.py
@@ -33,6 +33,7 @@ import json
 import os
 import pathlib
 import platform
+import posixpath
 import re
 import urllib.parse
 from itertools import chain
@@ -444,7 +445,7 @@ class _ArtifactoryFlavour(object if IS_PYTHON_3_12_OR_NEWER else pathlib._Flavou
     sep = "/"
     altsep = "/"
     has_drv = True
-    pathmod = pathlib.posixpath
+    pathmod = posixpath
     is_supported = True
 
     def _get_base_url(self, url):

(The underlying issue here is the changed pathlib API, pathmod seems not to have any effect on the actual issue as long as it's valid code)

Using this minimal example:

from artifactory import ArtifactoryPath

path = ArtifactoryPath("https://mysite/artifactory")
print(path)

I get correct results on Python <= 3.12:

https://mysite/artifactory

but a broken path on Python 3.13:

https:/mysite/artifactory
     ^^^
  Note the missing '/'

This causes invalid URLs on subsequent usages, which eventually end in a type error ("TypeError: must be str, not NoneType"; see below)

Running this projects tests on Python 3.13 confirms broken paths and/or resulting errors:

[…]
   File "/home/runner/work/artifactory/artifactory/artifactory.py", line 707, in rest_get
    url = quote_url(url)
  File "/home/runner/work/artifactory/artifactory/artifactory.py", line 424, in quote_url
    quoted_path = requests.utils.quote(url.partition(parsed_url.host)[2])
                                       ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
TypeError: must be str, not NoneType


[…]
 AssertionError: 'http:/b/artifactory/reponame/path.txt' != 'http://b/artifactory/reponame/path.txt'
- http:/b/artifactory/reponame/path.txt
+ http://b/artifactory/reponame/path.txt
?      +

@richardxia
Copy link

I did some further debugging, and figured out why the URL parsing is now broken under Python 3.13, and I also have a very small patch that will get the artifactory unit tests passes. It looks like between Python 3.12 and 3.13, a couple of patches12 ended up renaming the private PurePath._flavour class attribute to parser, which is now documented as a public API. Although I'm not an expert on the internals of pathlib, I don't believe the semantics have really changed, so it's just a name change, and it still points to basically either posixpath or ntpath, which are the POSIX and Windows path modules, respectively.

From what I can understand from the artifactory codebase, the _ArtifactoryFlavour class is meant to mimic that interface, and both PureArtifactoryPath and ArtifactorySaaSPath set their _flavour class attributes to point to custom flavours. Then, when invoking certain methods on the artifactory subclasses of PurePath, it relies on the superclass implementations of some methods to call cls._flavour.<foobar>, which will grab the versions of those methods and attributes defined on _ArtifactoryFlavour, not posixpath or ntpath. Here is one such example, where in Python 3.12, PurePath._parse_paths() calls cls._flavour.splitroot(): https://github.com/python/cpython/blob/v3.12.9/Lib/pathlib.py#L395

The issue is that when Python renamed _flavour to parser, it updated those calls as well, so the previous example now became cls.parser.splitroot(): https://github.com/python/cpython/blob/v3.13.2/Lib/pathlib/_local.py#L265. Accessing cls.parser does not cause an AttributeError because it is set by PurePath, but rather than pointing to _ArtifactoryFlavour, it points to posixpath or ntpath. Because it's not getting _ArtifactoryFlavour, it ends up parsing the URLs in a different way than the artifactory library needs it to be, resulting in these unit test failures.

A really quick and dirty fix is to just simultaneously define _flavour and parser on PureArtifactoryPath and ArtifactorySaaSPath:

diff --git a/artifactory.py b/artifactory.py
index 0c4b63c..7c440c6 100755
--- a/artifactory.py
+++ b/artifactory.py
@@ -33,6 +33,7 @@ import json
 import os
 import pathlib
 import platform
+import posixpath
 import re
 import urllib.parse
 from itertools import chain
@@ -444,7 +445,7 @@ class _ArtifactoryFlavour(object if IS_PYTHON_3_12_OR_NEWER else pathlib._Flavou
     sep = "/"
     altsep = "/"
     has_drv = True
-    pathmod = pathlib.posixpath
+    pathmod = posixpath
     is_supported = True

     def _get_base_url(self, url):
@@ -1500,6 +1501,7 @@ class PureArtifactoryPath(pathlib.PurePath):
     """

     _flavour = _artifactory_flavour
+    parser = _flavour
     __slots__ = ()

     def _init(self, *args):
@@ -2662,6 +2664,7 @@ class ArtifactorySaaSPath(ArtifactoryPath):
     """Class for SaaS Artifactory"""

     _flavour = _saas_artifactory_flavour
+    parser = _flavour


 class ArtifactoryBuild:

When I have that patch applied locally, it allows the unit tests to pass on Python 3.13. I think it should be safe to have both aliased attributes defined, since each version of Python will only look at the attribute it cares about. I can prepare a pull request with these changes, and we can discuss there whether we want to make any tweaks to it.

Footnotes

  1. https://github.com/python/cpython/commit/c6c5665ee0c0a5ddc96da255c9a62daa332c32b3 (rename _flavour to pathmod)

  2. https://github.com/python/cpython/commit/752e18389ed03087b51b38eac9769ef8dfd167b7 (rename pathmod to parser)

@allburov
Copy link
Member

allburov commented Feb 21, 2025

Hi! Thank you for PR!

Let's wait for the community feedback about the fix and we can merge it 🙏

pip install git+https://github.com/devopshq/artifactory@python-3-13

@allburov
Copy link
Member

Feature branch for Python 3.13 support: https://github.com/devopshq/artifactory/tree/python-3-13
Please run tests with your code (how you use it) and if you find a bug (and the fix, ideally) - create a PR there.
When it's stable enough - we'll merge and release it

@Cyberwizzard
Copy link

Works for me on 3.13 - cheers!

@SillieWous
Copy link

I just tried it. Although I am not getting the attribute error, path.glob('**/*') does not seem to work correctly anymore in 3.13. It does not return any path/file eventhough visiting the url manually does show there are files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help Wanted We will be glad if somebody proposes a solution via PR
5 participants