diff --git a/.github/workflows/package-macos.yml b/.github/workflows/package-macos.yml index 41a4061cff..63c199ba47 100644 --- a/.github/workflows/package-macos.yml +++ b/.github/workflows/package-macos.yml @@ -11,8 +11,8 @@ jobs: matrix: setup: - macos-deployment-version: '11.0' - python-version: 3.12.1-macos11 - python-sha256sum: 6178e42679eb83196240fc58b1438f481c32c2b0557f28ccf43aa7b1b80b7c4a + python-version: 3.13.1-macos11 + python-sha256sum: 67c6f0a3190851e0013214d5abd725a42ec398ff1b50eec47826820fd052d86b env: DISCID_VERSION: 0.6.4 DISCID_SHA256SUM: 829133dd38acbdaa2b989de59e256c8d139ac34cb4dd4b8fd3c9d55a97c824f3 diff --git a/.github/workflows/package-pypi.yml b/.github/workflows/package-pypi.yml index 7e50e98d2b..2cafcaf02d 100644 --- a/.github/workflows/package-pypi.yml +++ b/.github/workflows/package-pypi.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 - name: Install dependencies (Linux) if: runner.os == 'linux' run: | @@ -77,7 +77,7 @@ jobs: fail-fast: false matrix: os: [macos-13, windows-2019] - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/package-windows.yml b/.github/workflows/package-windows.yml index 2766fccef9..a0f9cb7c78 100644 --- a/.github/workflows/package-windows.yml +++ b/.github/workflows/package-windows.yml @@ -26,7 +26,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 - name: Setup Windows build environment run: | & .\scripts\package\win-setup.ps1 ` diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 20132da7ab..023e44fce2 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 - uses: actions/download-artifact@v4 with: name: macos-app-10.14 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2de7727790..ff070121ac 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,10 +9,7 @@ jobs: strategy: matrix: os: [macos-13, ubuntu-latest, windows-2019] - python-version: ['3.9', '3.10', '3.11', '3.12'] - include: - - os: macos-13 - python-version: '3.9' + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} @@ -103,7 +100,7 @@ jobs: strategy: matrix: os: [macos-13, ubuntu-latest, windows-latest] - python-version: ['3.9', '3.12'] + python-version: ['3.9', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/validate-xdg-metadata.yml b/.github/workflows/validate-xdg-metadata.yml index c969dc033f..dff0dbf050 100644 --- a/.github/workflows/validate-xdg-metadata.yml +++ b/.github/workflows/validate-xdg-metadata.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - name: Install utils run: | sudo apt-get update diff --git a/picard/log.py b/picard/log.py index b2b1b25580..1182f3fed1 100644 --- a/picard/log.py +++ b/picard/log.py @@ -3,7 +3,7 @@ # Picard, the next-generation MusicBrainz tagger # # Copyright (C) 2007, 2011 Lukáš Lalinský -# Copyright (C) 2008-2010, 2019, 2021-2023 Philipp Wolfer +# Copyright (C) 2008-2010, 2019, 2021-2024 Philipp Wolfer # Copyright (C) 2012-2013 Michael Wiencek # Copyright (C) 2013, 2015, 2018-2021, 2023-2024 Laurent Monin # Copyright (C) 2016-2018 Sambhav Kothari @@ -36,7 +36,7 @@ import logging from pathlib import ( Path, - PurePosixPath, + PurePath, ) from threading import Lock @@ -200,7 +200,7 @@ def name_filter(record): # way that the final `__init__.py` file is removed. if len(parts) > 1 and parts[-1] + '.zip' == parts[-2]: del parts[-1] - record.name = str(PurePosixPath(*parts)) + record.name = str(PurePath(*parts)) return True diff --git a/picard/util/__init__.py b/picard/util/__init__.py index f8e0c40024..1615243b71 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -268,7 +268,15 @@ def is_absolute_path(path): """Similar to os.path.isabs, but properly detects Windows shares as absolute paths See https://bugs.python.org/issue22302 """ - return os.path.isabs(path) or (IS_WIN and os.path.normpath(path).startswith("\\\\")) + if IS_WIN: + # Two backslashes indicate a UNC path. + if path.startswith("\\\\"): + return True + # Consider a single slash at the start not relative. This is the default + # for `os.path.isabs` since Python 3.13. + elif path.startswith("\\") or path.startswith("/"): + return False + return os.path.isabs(path) def samepath(path1, path2): diff --git a/requirements-build.txt b/requirements-build.txt index c375a7514e..2ee8b40a89 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1,4 +1,4 @@ Babel>=2.10.0 -PyInstaller==6.7.0 +PyInstaller==6.11.1 pytest>=8.1 setuptools>=62.4.0 diff --git a/test/test_log.py b/test/test_log.py index bdfad8cfa6..e42af62378 100644 --- a/test/test_log.py +++ b/test/test_log.py @@ -4,7 +4,7 @@ # # Copyright (C) 2021 Gabriel Ferreira # Copyright (C) 2021, 2024 Laurent Monin -# Copyright (C) 2021 Philipp Wolfer +# Copyright (C) 2021, 2024 Philipp Wolfer # Copyright (C) 2024 Bob Swift # # This program is free software; you can redistribute it and/or @@ -25,7 +25,7 @@ from collections import deque from dataclasses import dataclass from pathlib import ( - PurePosixPath, + PurePath, PureWindowsPath, ) import unittest @@ -135,8 +135,8 @@ class FakeRecord: @unittest.skipIf(IS_WIN, "Posix test") -@patch('picard.log.picard_module_path', PurePosixPath('/path1/path2')) -@patch('picard.log.USER_PLUGIN_DIR', PurePosixPath('/user/picard/plugins')) +@patch('picard.log.picard_module_path', PurePath('/path1/path2')) +@patch('picard.log.USER_PLUGIN_DIR', PurePath('/user/picard/plugins')) class NameFilterTestRel(PicardTestCase): def test_1(self): @@ -202,8 +202,8 @@ def test_plugin_path_short_4(self): @unittest.skipIf(IS_WIN, "Posix test") -@patch('picard.log.picard_module_path', PurePosixPath('/picard')) -@patch('picard.log.USER_PLUGIN_DIR', PurePosixPath('/user/picard/plugins/')) +@patch('picard.log.picard_module_path', PurePath('/picard')) +@patch('picard.log.USER_PLUGIN_DIR', PurePath('/user/picard/plugins/')) class NameFilterTestAbs(PicardTestCase): def test_1(self): @@ -270,8 +270,8 @@ def test_plugin_path_short_4(self): @unittest.skipIf(IS_WIN, "Posix test") -@patch('picard.log.picard_module_path', PurePosixPath('/path1/path2/')) # incorrect, but testing anyway -@patch('picard.log.USER_PLUGIN_DIR', PurePosixPath('/user/picard/plugins')) +@patch('picard.log.picard_module_path', PurePath('/path1/path2/')) # incorrect, but testing anyway +@patch('picard.log.USER_PLUGIN_DIR', PurePath('/user/picard/plugins')) class NameFilterTestEndingSlash(PicardTestCase): def test_1(self): @@ -282,13 +282,13 @@ def test_1(self): @unittest.skipUnless(IS_WIN, "Windows test") @patch('picard.log.picard_module_path', PureWindowsPath('C:\\path1\\path2')) -@patch('picard.log.USER_PLUGIN_DIR', PurePosixPath('C:\\user\\picard\\plugins')) +@patch('picard.log.USER_PLUGIN_DIR', PurePath('C:\\user\\picard\\plugins')) class NameFilterTestRelWin(PicardTestCase): def test_1(self): record = FakeRecord(name=None, pathname='C:/path1/path2/module/file.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, 'module/file') + self.assertEqual(record.name, 'module\\file') def test_2(self): record = FakeRecord(name=None, pathname='C:/path1/path2/module/__init__.py') @@ -298,7 +298,7 @@ def test_2(self): def test_3(self): record = FakeRecord(name=None, pathname='C:/path1/path2/module/subpath/file.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, 'module/subpath/file') + self.assertEqual(record.name, 'module\\subpath\\file') def test_4(self): record = FakeRecord(name=None, pathname='') @@ -308,64 +308,64 @@ def test_4(self): def test_5(self): record = FakeRecord(name=None, pathname='C:/path1/path2/__init__/module/__init__.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '__init__/module') + self.assertEqual(record.name, '__init__\\module') def test_plugin_path_long_1(self): DebugOpt.PLUGIN_FULLPATH.enabled = True record = FakeRecord(name=None, pathname='C:/user/picard/plugins/path3/plugins/plugin.zip') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/user/picard/plugins/path3/plugins/plugin') + self.assertEqual(record.name, '\\user\\picard\\plugins\\path3\\plugins\\plugin') def test_plugin_path_long_2(self): DebugOpt.PLUGIN_FULLPATH.enabled = True record = FakeRecord(name=None, pathname='C:/user/picard/plugins/path3/plugins/plugin.zip/xxx.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/user/picard/plugins/path3/plugins/plugin.zip/xxx') + self.assertEqual(record.name, '\\user\\picard\\plugins\\path3\\plugins\\plugin.zip\\xxx') def test_plugin_path_short_1(self): DebugOpt.PLUGIN_FULLPATH.enabled = False record = FakeRecord(name=None, pathname='C:/user/picard/plugins/path3/plugins/plugin.zip') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, 'plugins/path3/plugins/plugin') + self.assertEqual(record.name, 'plugins\\path3\\plugins\\plugin') def test_plugin_path_short_2(self): DebugOpt.PLUGIN_FULLPATH.enabled = False record = FakeRecord(name=None, pathname='C:/user/picard/plugins/path3/plugins/plugin.zip/xxx.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, 'plugins/path3/plugins/plugin.zip/xxx') + self.assertEqual(record.name, 'plugins\\path3\\plugins\\plugin.zip\\xxx') def test_plugin_path_short_3(self): DebugOpt.PLUGIN_FULLPATH.enabled = False record = FakeRecord(name=None, pathname='C:/user/picard/plugins/path3/plugins/myplugin.zip/myplugin.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, 'plugins/path3/plugins/myplugin.zip') + self.assertEqual(record.name, 'plugins\\path3\\plugins\\myplugin.zip') def test_plugin_path_short_4(self): DebugOpt.PLUGIN_FULLPATH.enabled = False record = FakeRecord(name=None, pathname='C:/user/picard/plugins/path3/plugins/myplugin.zip/__init__.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, 'plugins/path3/plugins/myplugin.zip') + self.assertEqual(record.name, 'plugins\\path3\\plugins\\myplugin.zip') @unittest.skipUnless(IS_WIN, "Windows test") @patch('picard.log.picard_module_path', PureWindowsPath('C:\\picard')) -@patch('picard.log.USER_PLUGIN_DIR', PurePosixPath('C:\\user\\picard/plugins')) +@patch('picard.log.USER_PLUGIN_DIR', PurePath('C:\\user\\picard\\plugins')) class NameFilterTestAbsWin(PicardTestCase): def test_1(self): record = FakeRecord(name=None, pathname='C:/path/module/file.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path/module/file') + self.assertEqual(record.name, '\\path\\module\\file') def test_2(self): record = FakeRecord(name=None, pathname='C:/path/module/__init__.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path/module') + self.assertEqual(record.name, '\\path\\module') def test_3(self): record = FakeRecord(name=None, pathname='C:/path/module/subpath/file.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path/module/subpath/file') + self.assertEqual(record.name, '\\path\\module\\subpath\\file') def test_4(self): record = FakeRecord(name=None, pathname='') @@ -376,45 +376,45 @@ def test_plugin_path_long_1(self): DebugOpt.PLUGIN_FULLPATH.enabled = True record = FakeRecord(name=None, pathname='C:/path1/path2/plugins/plugin.zip') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path1/path2/plugins/plugin') + self.assertEqual(record.name, '\\path1\\path2\\plugins\\plugin') def test_plugin_path_long_2(self): DebugOpt.PLUGIN_FULLPATH.enabled = True record = FakeRecord(name=None, pathname='C:/path1/path2/plugins/plugin.zip/xxx.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path1/path2/plugins/plugin.zip/xxx') + self.assertEqual(record.name, '\\path1\\path2\\plugins\\plugin.zip\\xxx') def test_plugin_path_short_1(self): DebugOpt.PLUGIN_FULLPATH.enabled = False record = FakeRecord(name=None, pathname='C:/path1/path2/plugins/plugin.zip') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path1/path2/plugins/plugin') + self.assertEqual(record.name, '\\path1\\path2\\plugins\\plugin') def test_plugin_path_short_2(self): DebugOpt.PLUGIN_FULLPATH.enabled = False record = FakeRecord(name=None, pathname='C:/path1/path2/plugins/plugin.zip/xxx.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path1/path2/plugins/plugin.zip/xxx') + self.assertEqual(record.name, '\\path1\\path2\\plugins\\plugin.zip\\xxx') def test_plugin_path_short_3(self): DebugOpt.PLUGIN_FULLPATH.enabled = False record = FakeRecord(name=None, pathname='C:/path1/path2/plugins/myplugin.zip/myplugin.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path1/path2/plugins/myplugin.zip') + self.assertEqual(record.name, '\\path1\\path2\\plugins\\myplugin.zip') def test_plugin_path_short_4(self): DebugOpt.PLUGIN_FULLPATH.enabled = False record = FakeRecord(name=None, pathname='C:/path1/path2/plugins/myplugin.zip/__init__.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path1/path2/plugins/myplugin.zip') + self.assertEqual(record.name, '\\path1\\path2\\plugins\\myplugin.zip') @unittest.skipUnless(IS_WIN, "Windows test") @patch('picard.log.picard_module_path', PureWindowsPath('C:\\path1\\path2\\')) # incorrect, but testing anyway -@patch('picard.log.USER_PLUGIN_DIR', PurePosixPath('C:\\user\\picard\\plugins')) +@patch('picard.log.USER_PLUGIN_DIR', PurePath('C:\\user\\picard\\plugins')) class NameFilterTestEndingSlashWin(PicardTestCase): def test_1(self): record = FakeRecord(name=None, pathname='C:/path3/module/file.py') self.assertTrue(name_filter(record)) - self.assertEqual(record.name, '/path3/module/file') + self.assertEqual(record.name, '\\path3\\module\\file') diff --git a/test/test_utils.py b/test/test_utils.py index 2b5068282c..bfff002d8b 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -400,6 +400,7 @@ def test_remove_windows_drive(self): class IsAbsolutePathTest(PicardTestCase): + @unittest.skipIf(IS_WIN, "POSIX test") def test_is_absolute(self): self.assertTrue(is_absolute_path('/foo/bar')) self.assertFalse(is_absolute_path('foo/bar')) @@ -410,7 +411,11 @@ def test_is_absolute(self): def test_is_absolute_windows(self): self.assertTrue(is_absolute_path('D:/foo/bar')) self.assertTrue(is_absolute_path('D:\\foo\\bar')) - self.assertTrue(is_absolute_path('\\foo\\bar')) + self.assertFalse(is_absolute_path('\\foo\\bar')) + self.assertFalse(is_absolute_path('/foo/bar')) + self.assertFalse(is_absolute_path('foo/bar')) + self.assertFalse(is_absolute_path('./foo/bar')) + self.assertFalse(is_absolute_path('../foo/bar')) # Paths to Windows shares self.assertTrue(is_absolute_path('\\\\foo\\bar')) self.assertTrue(is_absolute_path('\\\\foo\\bar\\'))