From 6fe9aa632b2147eaf7278230def9ea735eb73f4e Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen Date: Sat, 2 Jan 2021 00:48:32 +0100 Subject: [PATCH 001/138] Fix for user play count and user loved --- src/pylast/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 05c0361..20a087c 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -888,6 +888,9 @@ class _Request: if self.network.limit_rate: self.network._delay_call() + username = self.params.pop("username", None) + username = f"?username={username}" if username is not None else "" + data = [] for name in self.params.keys(): data.append("=".join((name, quote_plus(_string(self.params[name]))))) @@ -911,7 +914,7 @@ class _Request: try: conn.request( method="POST", - url="https://" + host_name + host_subdir, + url=f"https://{host_name}{host_subdir}{username}", body=data, headers=headers, ) @@ -922,7 +925,7 @@ class _Request: conn = HTTPSConnection(context=SSL_CONTEXT, host=host_name) try: - conn.request(method="POST", url=host_subdir, body=data, headers=headers) + conn.request(method="POST", url=f'{host_subdir}{username}', body=data, headers=headers) except Exception as e: raise NetworkError(self.network, e) @@ -1494,7 +1497,7 @@ class _Opus(_Taggable): self.artist = Artist(artist, self.network) self.title = title - self.username = username + self.username = username if username else network.username # Default to current user self.info = info def __repr__(self): From 0c546976b9c640c1c38acc57ec27dbebb29127a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 2 Jan 2021 00:06:15 +0000 Subject: [PATCH 002/138] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pylast/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 20a087c..bec13d5 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -925,7 +925,12 @@ class _Request: conn = HTTPSConnection(context=SSL_CONTEXT, host=host_name) try: - conn.request(method="POST", url=f'{host_subdir}{username}', body=data, headers=headers) + conn.request( + method="POST", + url=f"{host_subdir}{username}", + body=data, + headers=headers, + ) except Exception as e: raise NetworkError(self.network, e) @@ -1497,7 +1502,9 @@ class _Opus(_Taggable): self.artist = Artist(artist, self.network) self.title = title - self.username = username if username else network.username # Default to current user + self.username = ( + username if username else network.username + ) # Default to current user self.info = info def __repr__(self): From 4e645ca134d1aec6fb965fb5bc8e9189a401f6eb Mon Sep 17 00:00:00 2001 From: Chandler Swift Date: Tue, 27 Apr 2021 15:29:26 -0500 Subject: [PATCH 003/138] Set get_top_tracks limit even if it's `None` To get an unlimited number of top tracks, `_get_things` expects `params['limit']` to be set to `None`. However, this can't happen here because `None` is falsy. Fixes #366. --- src/pylast/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 1341de0..40f7468 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -2531,8 +2531,7 @@ class User(_Chartable): params = self._get_params() params["period"] = period - if limit: - params["limit"] = limit + params["limit"] = limit return self._get_things("getTopTracks", Track, params, cacheable, stream=stream) From f3467bca36d8f9de95f9c5a5c36c26b61b57530b Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 18 Jun 2019 15:33:59 +0300 Subject: [PATCH 004/138] Use POST for writes (with api_sig), GET for reads --- src/pylast/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 1341de0..11bf1d8 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -904,6 +904,13 @@ class _Request: data.append("=".join((name, quote_plus(_string(self.params[name]))))) data = "&".join(data) + if "api_sig" in self.params.keys(): + method = "POST" + url_parameters = "" + else: + method = "GET" + url_parameters = "?" + data + headers = { "Content-type": "application/x-www-form-urlencoded", "Accept-Charset": "utf-8", @@ -921,8 +928,8 @@ class _Request: try: conn.request( - method="POST", - url="https://" + host_name + host_subdir, + method=method, + url="https://" + host_name + host_subdir + url_parameters, body=data, headers=headers, ) @@ -933,7 +940,12 @@ class _Request: conn = HTTPSConnection(context=SSL_CONTEXT, host=host_name) try: - conn.request(method="POST", url=host_subdir, body=data, headers=headers) + conn.request( + method=method, + url=host_subdir + url_parameters, + body=data, + headers=headers, + ) except Exception as e: raise NetworkError(self.network, e) from e From 0f26b6cc8ae7d99609d61f9218212e65f9f8ff27 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 18 Jun 2019 17:52:59 +0300 Subject: [PATCH 005/138] Add more logging --- src/pylast/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 11bf1d8..317de7e 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -903,6 +903,7 @@ class _Request: for name in self.params.keys(): data.append("=".join((name, quote_plus(_string(self.params[name]))))) data = "&".join(data) + logger.debug(data) if "api_sig" in self.params.keys(): method = "POST" @@ -910,6 +911,7 @@ class _Request: else: method = "GET" url_parameters = "?" + data + logger.debug(method) headers = { "Content-type": "application/x-www-form-urlencoded", @@ -966,6 +968,7 @@ class _Request: self._check_response_for_errors(response_text) finally: conn.close() + logger.debug(response_text) return response_text def execute(self, cacheable=False): From a516a44c329d6921755ebd7cd7e5fda9e851ad95 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 30 Apr 2021 22:53:37 +0300 Subject: [PATCH 006/138] New changes are documented in GH Releases --- CHANGELOG.md | 9 ++++++--- RELEASING.md | 2 -- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b4ede3..2518770 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ # Changelog -All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 4.2.1 and newer + +See GitHub Releases: + +- https://github.com/pylast/pylast/releases ## [4.2.0] - 2021-03-14 diff --git a/RELEASING.md b/RELEASING.md index 7e3cdfc..8181754 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -10,8 +10,6 @@ * [ ] Check next tag is correct, amend if needed -* [ ] Copy text into [`CHANGELOG.md`](CHANGELOG.md) - * [ ] Publish release * [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions?query=workflow%3ADeploy) From ce2c1e6f769d262300c88d9c08f0df645ea6bd24 Mon Sep 17 00:00:00 2001 From: Tran Tieu Binh Date: Sat, 29 May 2021 13:57:30 +0700 Subject: [PATCH 007/138] Remove artist.shout("<3") There is no shout() method for the artist object. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index fb05c3b..c4f4ad6 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,6 @@ network = pylast.LastFMNetwork( # Now you can use that object everywhere artist = network.get_artist("System of a Down") -artist.shout("<3") track = network.get_track("Iron Maiden", "The Nomad") From 1a35601f513b7902e623da2704f0c023f09f0e3d Mon Sep 17 00:00:00 2001 From: Tran Tieu Binh Date: Sat, 29 May 2021 14:24:41 +0700 Subject: [PATCH 008/138] Remove blank line --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index c4f4ad6..77e381c 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,6 @@ network = pylast.LastFMNetwork( ) # Now you can use that object everywhere -artist = network.get_artist("System of a Down") - - track = network.get_track("Iron Maiden", "The Nomad") track.love() track.add_tags(("awesome", "favorite")) From 20cd3ff4758a51b710bcc007f1d4cff3d7e3ec35 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 Aug 2021 20:28:45 +0300 Subject: [PATCH 009/138] Update pre-commit and add quarterly autoupdate_schedule --- .pre-commit-config.yaml | 21 ++++++++++++--------- src/pylast/__init__.py | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a363863..3686a37 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.10.0 + rev: v2.23.1 hooks: - id: pyupgrade args: ["--py36-plus"] - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 21.7b0 hooks: - id: black args: ["--target-version", "py36"] @@ -15,35 +15,38 @@ repos: types: [] - repo: https://github.com/asottile/blacken-docs - rev: v1.9.2 + rev: v1.10.0 hooks: - id: blacken-docs args: ["--target-version", "py36"] additional_dependencies: [black==20.8b1] - repo: https://github.com/PyCQA/isort - rev: 5.7.0 + rev: 5.9.3 hooks: - id: isort - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.4 + - repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.7.1 + rev: v1.9.0 hooks: - id: python-check-blanket-noqa - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: check-merge-conflict - id: check-yaml - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.5.0 + rev: 0.5.1 hooks: - id: tox-ini-fmt + +ci: + autoupdate_schedule: quarterly diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 8105d52..e192fa2 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -1355,7 +1355,7 @@ class _Taggable(_BaseObject): self.remove_tag(tag) def clear_tags(self): - """Clears all the user-set tags. """ + """Clears all the user-set tags.""" self.remove_tags(*(self.get_tags())) @@ -1702,7 +1702,7 @@ class Artist(_Taggable): self.info = info def __repr__(self): - return "pylast.Artist({}, {})".format(repr(self.get_name()), repr(self.network)) + return f"pylast.Artist({repr(self.get_name())}, {repr(self.network)})" def __unicode__(self): return str(self.get_name()) @@ -1886,7 +1886,7 @@ class Country(_BaseObject): self.name = name def __repr__(self): - return "pylast.Country({}, {})".format(repr(self.name), repr(self.network)) + return f"pylast.Country({repr(self.name)}, {repr(self.network)})" @_string_output def __str__(self): @@ -1902,7 +1902,7 @@ class Country(_BaseObject): return {"country": self.get_name()} def get_name(self): - """Returns the country name. """ + """Returns the country name.""" return self.name @@ -1964,7 +1964,7 @@ class Library(_BaseObject): self.user = User(user, self.network) def __repr__(self): - return "pylast.Library({}, {})".format(repr(self.user), repr(self.network)) + return f"pylast.Library({repr(self.user)}, {repr(self.network)})" @_string_output def __str__(self): @@ -2010,7 +2010,7 @@ class Tag(_Chartable): self.name = name def __repr__(self): - return "pylast.Tag({}, {})".format(repr(self.name), repr(self.network)) + return f"pylast.Tag({repr(self.name)}, {repr(self.network)})" @_string_output def __str__(self): @@ -2026,7 +2026,7 @@ class Tag(_Chartable): return {self.ws_prefix: self.get_name()} def get_name(self, properly_capitalized=False): - """Returns the name of the tag. """ + """Returns the name of the tag.""" if properly_capitalized: self.name = _extract( @@ -2149,12 +2149,12 @@ class Track(_Opus): return Album(_extract(node, "artist"), _extract(node, "title"), self.network) def love(self): - """Adds the track to the user's loved tracks. """ + """Adds the track to the user's loved tracks.""" self._request(self.ws_prefix + ".love") def unlove(self): - """Remove the track to the user's loved tracks. """ + """Remove the track to the user's loved tracks.""" self._request(self.ws_prefix + ".unlove") @@ -2220,7 +2220,7 @@ class User(_Chartable): self.name = user_name def __repr__(self): - return "pylast.User({}, {})".format(repr(self.name), repr(self.network)) + return f"pylast.User({repr(self.name)}, {repr(self.network)})" @_string_output def __str__(self): @@ -2259,7 +2259,7 @@ class User(_Chartable): return self.name def get_friends(self, limit=50, cacheable=False, stream=False): - """Returns a list of the user's friends. """ + """Returns a list of the user's friends.""" def _get_friends(): for node in _collect_nodes( @@ -2604,7 +2604,7 @@ class User(_Chartable): return self.network._get_url(domain_name, "user") % {"name": name} def get_library(self): - """Returns the associated Library object. """ + """Returns the associated Library object.""" return Library(self, self.network) From c8a64dbee9462678b2451b12def06baf1bff55b9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 Aug 2021 20:42:20 +0300 Subject: [PATCH 010/138] Test image is now gif --- tests/test_album.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_album.py b/tests/test_album.py index d6bf3e1..e3ca4f7 100755 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -95,4 +95,4 @@ class TestPyLastAlbum(TestPyLastWithLastFm): # Assert self.assert_startswith(image, "https://") - self.assert_endswith(image, ".png") + self.assert_endswith(image, ".gif") From 72491f7a9920e0d0d9752a634e59548fb5588e4a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 Aug 2021 20:46:56 +0300 Subject: [PATCH 011/138] Last.fm now even skips an empty when no bio --- src/pylast/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index e192fa2..c24cd43 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -1798,9 +1798,14 @@ class Artist(_Taggable): else: params = None - return self._extract_cdata_from_request( - self.ws_prefix + ".getInfo", section, params - ) + try: + bio = self._extract_cdata_from_request( + self.ws_prefix + ".getInfo", section, params + ) + except IndexError: + bio = None + + return bio def get_bio_published_date(self): """Returns the date on which the artist's biography was published.""" From ddb1b1e501f70d1e9beee853770d7ec6bcdabc86 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 Aug 2021 20:50:21 +0300 Subject: [PATCH 012/138] Fix test --- tests/test_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_network.py b/tests/test_network.py index b45fafa..9c3ad44 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -265,7 +265,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert assert isinstance(artist, pylast.Artist) - assert artist.name == "MusicBrainz Test Artist" + assert artist.name in ("MusicBrainz Test Artist", "MusicBrainzz Test Artist") def test_track_mbid(self): # Arrange From a850f093f0a2df5c8904c740e2f6b7bd3e9c83c9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 Aug 2021 21:09:28 +0300 Subject: [PATCH 013/138] track.getInfo with mbid is broken at Last.fm: https://support.last.fm/t/track-getinfo-with-mbid-returns-6-track-not-found/47905 --- tests/test_network.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_network.py b/tests/test_network.py index 9c3ad44..051c0d8 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -267,6 +267,8 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert isinstance(artist, pylast.Artist) assert artist.name in ("MusicBrainz Test Artist", "MusicBrainzz Test Artist") + @pytest.mark.xfail(reason="Broken at Last.fm: Track not found") + # https://support.last.fm/t/track-getinfo-with-mbid-returns-6-track-not-found/47905 def test_track_mbid(self): # Arrange mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0" From 73e3b1b9eddf183164d1f1043bf24d69d0d23d13 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 16:45:32 +0000 Subject: [PATCH 014/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.23.1 → v2.29.0](https://github.com/asottile/pyupgrade/compare/v2.23.1...v2.29.0) - [github.com/psf/black: 21.7b0 → 21.9b0](https://github.com/psf/black/compare/21.7b0...21.9b0) - [github.com/asottile/blacken-docs: v1.10.0 → v1.11.0](https://github.com/asottile/blacken-docs/compare/v1.10.0...v1.11.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3686a37..a720261 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.23.1 + rev: v2.29.0 hooks: - id: pyupgrade args: ["--py36-plus"] - repo: https://github.com/psf/black - rev: 21.7b0 + rev: 21.9b0 hooks: - id: black args: ["--target-version", "py36"] @@ -15,7 +15,7 @@ repos: types: [] - repo: https://github.com/asottile/blacken-docs - rev: v1.10.0 + rev: v1.11.0 hooks: - id: blacken-docs args: ["--target-version", "py36"] From 031b3ebbb1da1367613410d859ee1b3cacae5420 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 4 Oct 2021 20:36:05 +0300 Subject: [PATCH 015/138] Add support for Python 3.10 --- README.md | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 77e381c..b297c74 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ Or from requirements.txt: Note: -* pyLast 4.0+ supports Python 3.6-3.9. +* pyLast 4.3+ supports Python 3.6-3.10. +* pyLast 4.0 - 4.2 supports Python 3.6-3.9. * pyLast 3.2 - 3.3 supports Python 3.5-3.8. * pyLast 3.0 - 3.1 supports Python 3.5-3.7. * pyLast 2.2 - 2.4 supports Python 2.7.10+, 3.4-3.7. diff --git a/setup.py b/setup.py index a2d891f..b49e655 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ setup( "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", From 9072b98a18cd7ae3b553ddee85ebe683e7d9e7c4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 19 Oct 2021 13:08:53 +0300 Subject: [PATCH 016/138] Rename master to main, use 3.10 final, add workflow_dispatch --- .github/workflows/deploy.yml | 15 ++++++++------- .github/workflows/labels.yml | 7 +++++-- .github/workflows/lint.yml | 2 +- .github/workflows/release-drafter.yml | 7 ++++--- .github/workflows/test.yml | 12 ++++++------ README.md | 8 ++++---- RELEASING.md | 14 +++++++------- tox.ini | 13 +++++++------ 8 files changed, 42 insertions(+), 36 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1f65cb8..9b4297e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,14 +3,15 @@ name: Deploy on: push: branches: - - master + - main release: types: - published + workflow_dispatch: jobs: - build: - if: github.repository == 'pylast/pylast' + deploy: + if: github.repository_owner == 'pylast' runs-on: ubuntu-20.04 steps: @@ -29,18 +30,18 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - name: Install dependencies run: | python -m pip install -U pip - python -m pip install -U setuptools twine wheel + python -m pip install -U build twine wheel - name: Build package run: | python setup.py --version - python setup.py sdist --format=gztar bdist_wheel - twine check dist/* + python -m build + twine check --strict dist/* - name: Publish package to PyPI if: github.event.action == 'published' diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index e84c13e..c22c0d0 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -1,12 +1,15 @@ name: Sync labels + on: push: branches: - - master + - main paths: - .github/labels.yml + workflow_dispatch: + jobs: - build: + sync: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f092b74..bfe362b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,6 +1,6 @@ name: Lint -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: lint: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index f1d92f9..a806a43 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -4,14 +4,15 @@ on: push: # branches to consider in the event; optional, defaults to all branches: - - master + - main + workflow_dispatch: jobs: update_release_draft: - if: github.repository == 'pylast/pylast' + if: github.repository_owner == 'hugovk' runs-on: ubuntu-latest steps: - # Drafts your next release notes as pull requests are merged into "master" + # Drafts your next release notes as pull requests are merged into "main" - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e8b978a..7bda29d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,17 +1,17 @@ name: Test -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] env: FORCE_COLOR: 1 jobs: - build: + test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10-dev", "pypy3"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy3"] os: [ubuntu-20.04] include: # Include new variables for Codecov @@ -35,10 +35,10 @@ jobs: with: path: ${{ steps.pip-cache.outputs.dir }} key: - ${{ matrix.os }}-${{ matrix.python-version }}-v3-${{ + ${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }} restore-keys: | - ${{ matrix.os }}-${{ matrix.python-version }}-v3- + ${{ matrix.os }}-${{ matrix.python-version }}-v1- - name: Install dependencies run: | @@ -56,7 +56,7 @@ jobs: PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }} - name: Upload coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 with: flags: ${{ matrix.codecov-flag }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index b297c74..3c62148 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ pyLast [![Supported Python versions](https://img.shields.io/pypi/pyversions/pylast.svg)](https://pypi.org/project/pylast/) [![PyPI downloads](https://img.shields.io/pypi/dm/pylast.svg)](https://pypistats.org/packages/pylast) [![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions) -[![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/master/graph/badge.svg)](https://codecov.io/gh/pylast/pylast) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/main/graph/badge.svg)](https://codecov.io/gh/pylast/pylast) +[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![DOI](https://zenodo.org/badge/7803088.svg)](https://zenodo.org/badge/latestdoi/7803088) A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites @@ -98,12 +98,12 @@ track.add_tags(("awesome", "favorite")) More examples in hugovk/lastfm-tools and -[tests/](https://github.com/pylast/pylast/tree/master/tests). +[tests/](https://github.com/pylast/pylast/tree/main/tests). Testing ------- -The [tests/](https://github.com/pylast/pylast/tree/master/tests) directory contains +The [tests/](https://github.com/pylast/pylast/tree/main/tests) directory contains integration and unit tests with Last.fm, and plenty of code examples. For integration tests you need a test account at Last.fm that will become cluttered with diff --git a/RELEASING.md b/RELEASING.md index 8181754..452bca6 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,21 +1,21 @@ # Release Checklist -* [ ] Get master to the appropriate code release state. +- [ ] Get `main` to the appropriate code release state. [GitHub Actions](https://github.com/pylast/pylast/actions) should be running cleanly for - all merges to master. + all merges to `main`. [![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions) -* [ ] Edit release draft, adjust text if needed: +- [ ] Edit release draft, adjust text if needed: https://github.com/pylast/pylast/releases -* [ ] Check next tag is correct, amend if needed +- [ ] Check next tag is correct, amend if needed -* [ ] Publish release +- [ ] Publish release -* [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions?query=workflow%3ADeploy) +- [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions?query=workflow%3ADeploy) has deployed to [PyPI](https://pypi.org/project/pylast/#history) -* [ ] Check installation: +- [ ] Check installation: ```bash pip3 uninstall -y pylast && pip3 install -U pylast && python3 -c "import pylast; print(pylast.__version__)" diff --git a/tox.ini b/tox.ini index c19e202..0cdfe53 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] envlist = + lint py{py3, 310, 39, 38, 37, 36} [testenv] @@ -13,12 +14,6 @@ extras = commands = pytest -v -s -W all --cov pylast --cov tests --cov-report term-missing --random-order {posargs} -[testenv:venv] -deps = - ipdb -commands = - {posargs} - [testenv:lint] passenv = PRE_COMMIT_COLOR @@ -27,3 +22,9 @@ deps = pre-commit commands = pre-commit run --all-files --show-diff-on-failure + +[testenv:venv] +deps = + ipdb +commands = + {posargs} From e5b9f2aa1965572b0723f1d95b8ceef6be03f60b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 19 Oct 2021 13:11:21 +0300 Subject: [PATCH 017/138] Add colour to tests --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 0cdfe53..c04ab95 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = [testenv] passenv = + FORCE_COLOR PYLAST_API_KEY PYLAST_API_SECRET PYLAST_PASSWORD_HASH From c41f831d82c02166e887e32a63a5e19330b50ed5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 19 Oct 2021 13:17:20 +0300 Subject: [PATCH 018/138] Fix test --- tests/test_artist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_artist.py b/tests/test_artist.py index 4e8d694..a911882 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -22,7 +22,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): def test_artist_is_hashable(self): # Arrange - test_artist = self.network.get_artist("Test Artist") + test_artist = self.network.get_artist("Radiohead") artist = test_artist.get_similar(limit=2)[0].item assert isinstance(artist, pylast.Artist) From 05b4ad8c629a60a13cf183deca0acadb98004ac3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 19 Oct 2021 13:57:47 +0300 Subject: [PATCH 019/138] Disable the flaky write tests --- tests/test_pylast.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 26f799c..c7cd7b3 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -3,7 +3,6 @@ Integration (not unit) tests for pylast.py """ import os -import sys import time import pytest @@ -11,7 +10,7 @@ from flaky import flaky import pylast -WRITE_TEST = sys.version_info[:2] == (3, 9) +WRITE_TEST = False def load_secrets(): # pragma: no cover From ae7d4e36259f5663f42b3f9a9ae8b8bf4af5e0df Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 10 Nov 2021 10:53:05 +0200 Subject: [PATCH 020/138] Replace deprecated pypy3 with pypy-3.8 Committed via https://github.com/asottile/all-repos --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7bda29d..130a0a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy3"] + python-version: ["pypy-3.8", "3.6", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-20.04] include: # Include new variables for Codecov From 3a7c83998fe6af57f13140ce08ba4aac4a690912 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 10 Nov 2021 11:44:10 +0200 Subject: [PATCH 021/138] Replace MBID for missing test album with a real one --- tests/test_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_network.py b/tests/test_network.py index 051c0d8..2ed839f 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -245,7 +245,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): def test_album_mbid(self): # Arrange - mbid = "a6a265bf-9f81-4055-8224-f7ac0aa6b937" + mbid = "03c91c40-49a6-44a7-90e7-a700edf97a62" # Act album = self.network.get_album_by_mbid(mbid) @@ -253,7 +253,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert assert isinstance(album, pylast.Album) - assert album.title.lower() == "test" + assert album.title == "Believe" assert album_mbid == mbid def test_artist_mbid(self): From b3fb55586c20cba223f4814eb3c5b30baa40aebc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 21 Nov 2021 16:42:04 +0200 Subject: [PATCH 022/138] Convert setup.py to static setup.cfg and format with setup-cfg-fmt --- .pre-commit-config.yaml | 17 +++++++++----- setup.cfg | 49 +++++++++++++++++++++++++++++++++++++++++ setup.py | 39 ++------------------------------ 3 files changed, 62 insertions(+), 43 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a720261..2bf62a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.29.0 + rev: v2.29.1 hooks: - id: pyupgrade args: ["--py36-plus"] - repo: https://github.com/psf/black - rev: 21.9b0 + rev: 21.11b1 hooks: - id: black args: ["--target-version", "py36"] @@ -15,19 +15,19 @@ repos: types: [] - repo: https://github.com/asottile/blacken-docs - rev: v1.11.0 + rev: v1.12.0 hooks: - id: blacken-docs args: ["--target-version", "py36"] - additional_dependencies: [black==20.8b1] + additional_dependencies: [black==21.11b1] - repo: https://github.com/PyCQA/isort - rev: 5.9.3 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] @@ -43,6 +43,11 @@ repos: - id: check-merge-conflict - id: check-yaml + - repo: https://github.com/asottile/setup-cfg-fmt + rev: v1.20.0 + hooks: + - id: setup-cfg-fmt + - repo: https://github.com/tox-dev/tox-ini-fmt rev: 0.5.1 hooks: diff --git a/setup.cfg b/setup.cfg index 191fac9..dd25e90 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,52 @@ +[metadata] +name = pylast +description = A Python interface to Last.fm and Libre.fm +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/pylast/pylast +author = Amr Hassan and Contributors +author_email = amr.hassan@gmail.com +license = Apache-2.0 +license_file = LICENSE.txt +classifiers = + Development Status :: 5 - Production/Stable + License :: OSI Approved :: Apache Software License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Internet + Topic :: Multimedia :: Sound/Audio + Topic :: Software Development :: Libraries :: Python Modules +keywords = + Last.fm + music + scrobble + scrobbling + +[options] +packages = find: +python_requires = >=3.6 +package_dir = =src +setup_requires = + setuptools-scm + +[options.packages.find] +where = src + +[options.extras_require] +tests = + flaky + pytest + pytest-cov + pytest-random-order + pyyaml + [flake8] max_line_length = 88 diff --git a/setup.py b/setup.py index b49e655..668794a 100755 --- a/setup.py +++ b/setup.py @@ -1,47 +1,12 @@ -from setuptools import find_packages, setup - -with open("README.md") as f: - long_description = f.read() +from setuptools import setup -def local_scheme(version): +def local_scheme(version) -> str: """Skip the local version (eg. +xyz of 0.6.1.dev4+gdf99fe2) to be able to upload to Test PyPI""" return "" setup( - name="pylast", - description="A Python interface to Last.fm and Libre.fm", - long_description=long_description, - long_description_content_type="text/markdown", - author="Amr Hassan and Contributors", - author_email="amr.hassan@gmail.com", - url="https://github.com/pylast/pylast", - license="Apache2", - keywords=["Last.fm", "music", "scrobble", "scrobbling"], - packages=find_packages(where="src"), - package_dir={"": "src"}, use_scm_version={"local_scheme": local_scheme}, - setup_requires=["setuptools_scm"], - extras_require={ - "tests": ["flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml"] - }, - python_requires=">=3.6", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: Apache Software License", - "Topic :: Internet", - "Topic :: Multimedia :: Sound/Audio", - "Topic :: Software Development :: Libraries :: Python Modules", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - ], ) From 754d94374b1fbebb77400cdd0d763aaa587f4740 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 21 Nov 2021 17:44:49 +0200 Subject: [PATCH 023/138] Use actions/setup-python's pip cache --- .editorconfig | 1 - .github/release-drafter.yml | 1 - .github/workflows/deploy.yml | 10 ++-------- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 17 ++--------------- 5 files changed, 5 insertions(+), 26 deletions(-) diff --git a/.editorconfig b/.editorconfig index b71c07e..179fd45 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,7 +11,6 @@ charset = utf-8 [*.py] indent_size = 4 indent_style = space - trim_trailing_whitespace = true # Two-space indentation diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index b853342..67eccf9 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -23,7 +23,6 @@ exclude-labels: - "changelog: skip" template: | - $CHANGES version-resolver: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9b4297e..627be8b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,18 +19,12 @@ jobs: with: fetch-depth: 0 - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: deploy-${{ hashFiles('**/setup.py') }} - restore-keys: | - deploy- - - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.10" + cache: pip + cache-dependency-path: "setup.py" - name: Install dependencies run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bfe362b..769ea4e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,4 +9,4 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - - uses: pre-commit/action@v2.0.0 + - uses: pre-commit/action@v2.0.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 130a0a0..ba3aaf0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,21 +24,8 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" - - - name: Cache - uses: actions/cache@v2 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: - ${{ matrix.os }}-${{ matrix.python-version }}-v1-${{ - hashFiles('**/setup.py') }} - restore-keys: | - ${{ matrix.os }}-${{ matrix.python-version }}-v1- + cache: pip + cache-dependency-path: "setup.py" - name: Install dependencies run: | From 25cf4165ea938186c57f7743e682d3b8e6d62501 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 21 Nov 2021 17:44:55 +0200 Subject: [PATCH 024/138] Fix typo --- .github/workflows/release-drafter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index a806a43..cb11924 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -9,7 +9,7 @@ on: jobs: update_release_draft: - if: github.repository_owner == 'hugovk' + if: github.repository_owner == 'pylast' runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" From b48fbb4eb8c2d89b1c19291faca9b460a5cfa09d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 21 Nov 2021 18:07:40 +0200 Subject: [PATCH 025/138] Speedup: Use faster importlib.metadata for getting version --- setup.cfg | 3 +++ src/pylast/__init__.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index dd25e90..7b806d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,7 @@ long_description_content_type = text/markdown url = https://github.com/pylast/pylast author = Amr Hassan and Contributors author_email = amr.hassan@gmail.com +maintainer = Hugo van Kemenade license = Apache-2.0 license_file = LICENSE.txt classifiers = @@ -31,6 +32,8 @@ keywords = [options] packages = find: +install_requires = + importlib-metadata;python_version < '3.8' python_requires = >=3.6 package_dir = =src setup_requires = diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index c24cd43..43cecce 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -33,13 +33,18 @@ from http.client import HTTPSConnection from urllib.parse import quote_plus from xml.dom import Node, minidom -import pkg_resources +try: + # Python 3.8+ + import importlib.metadata as importlib_metadata +except ImportError: + # Python 3.7 and lower + import importlib_metadata __author__ = "Amr Hassan, hugovk, Mice Pápai" __copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2021 hugovk, 2017 Mice Pápai" __license__ = "apache2" __email__ = "amr.hassan@gmail.com" -__version__ = pkg_resources.get_distribution(__name__).version +__version__ = importlib_metadata.version(__name__) # 1 : This error does not exist From 8b66e6900480611386425d9b01d2d279fafcc36d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 21 Nov 2021 18:43:32 +0200 Subject: [PATCH 026/138] Drop support for soon-EOL Python 3.6 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 9 +++------ README.md | 21 ++++++++------------- setup.cfg | 3 +-- tox.ini | 2 +- 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba3aaf0..f133aeb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-20.04] include: # Include new variables for Codecov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2bf62a8..f9b4cb2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,22 +3,19 @@ repos: rev: v2.29.1 hooks: - id: pyupgrade - args: ["--py36-plus"] + args: [--py37-plus] - repo: https://github.com/psf/black rev: 21.11b1 hooks: - id: black - args: ["--target-version", "py36"] - # override until resolved: https://github.com/psf/black/issues/402 - files: \.pyi?$ - types: [] + args: [--target-version=py37] - repo: https://github.com/asottile/blacken-docs rev: v1.12.0 hooks: - id: blacken-docs - args: ["--target-version", "py36"] + args: [--target-version=py37] additional_dependencies: [black==21.11b1] - repo: https://github.com/PyCQA/isort diff --git a/README.md b/README.md index 3c62148..1232eb1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -pyLast -====== +# pyLast [![PyPI version](https://img.shields.io/pypi/v/pylast.svg)](https://pypi.org/project/pylast/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/pylast.svg)](https://pypi.org/project/pylast/) @@ -14,8 +13,7 @@ such as [Libre.fm](https://libre.fm/). Use the pydoc utility for help on usage or see [tests/](tests/) for examples. -Installation ------------- +## Installation Install via pip: @@ -32,11 +30,12 @@ python3 -m pip install -U git+https://github.com/pylast/pylast Or from requirements.txt: ```txt --e git://github.com/pylast/pylast.git#egg=pylast +-e https://github.com/pylast/pylast.git#egg=pylast ``` Note: +* pyLast 5.0+ supports Python 3.7-3.10. * pyLast 4.3+ supports Python 3.6-3.10. * pyLast 4.0 - 4.2 supports Python 3.6-3.9. * pyLast 3.2 - 3.3 supports Python 3.5-3.8. @@ -48,8 +47,7 @@ Note: * pyLast 0.5 supports Python 2, 3. * pyLast < 0.5 supports Python 2. -Features --------- +## Features * Simple public interface. * Access to all the data exposed by the Last.fm web services. @@ -60,8 +58,7 @@ Features * Support for other API-compatible networks like Libre.fm. -Getting started ---------------- +## Getting started Here's some simple code example to get you started. In order to create any object from pyLast, you need a `Network` object which represents a social music network that is @@ -100,8 +97,7 @@ More examples in hugovk/lastfm-tools and [tests/](https://github.com/pylast/pylast/tree/main/tests). -Testing -------- +## Testing The [tests/](https://github.com/pylast/pylast/tree/main/tests) directory contains integration and unit tests with Last.fm, and plenty of code examples. @@ -140,8 +136,7 @@ coverage html # for HTML report open htmlcov/index.html ``` -Logging -------- +## Logging To enable from your own code: diff --git a/setup.cfg b/setup.cfg index 7b806d9..5a80546 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -34,7 +33,7 @@ keywords = packages = find: install_requires = importlib-metadata;python_version < '3.8' -python_requires = >=3.6 +python_requires = >=3.7 package_dir = =src setup_requires = setuptools-scm diff --git a/tox.ini b/tox.ini index c04ab95..9022a23 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = lint - py{py3, 310, 39, 38, 37, 36} + py{py3, 310, 39, 38, 37} [testenv] passenv = From 129e4392fcc3ef4b6f5538f209717eec828b7f16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 16:58:33 +0000 Subject: [PATCH 027/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.29.1 → v2.31.0](https://github.com/asottile/pyupgrade/compare/v2.29.1...v2.31.0) - [github.com/psf/black: 21.11b1 → 21.12b0](https://github.com/psf/black/compare/21.11b1...21.12b0) - [github.com/pre-commit/pre-commit-hooks: v4.0.1 → v4.1.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.0.1...v4.1.0) - [github.com/tox-dev/tox-ini-fmt: 0.5.1 → 0.5.2](https://github.com/tox-dev/tox-ini-fmt/compare/0.5.1...0.5.2) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2bf62a8..4706c19 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.31.0 hooks: - id: pyupgrade args: ["--py36-plus"] - repo: https://github.com/psf/black - rev: 21.11b1 + rev: 21.12b0 hooks: - id: black args: ["--target-version", "py36"] @@ -38,7 +38,7 @@ repos: - id: python-check-blanket-noqa - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: check-merge-conflict - id: check-yaml @@ -49,7 +49,7 @@ repos: - id: setup-cfg-fmt - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.5.1 + rev: 0.5.2 hooks: - id: tox-ini-fmt From 9676714dcf6370eb19cf323234014fd5ddec3bc0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 12 Jan 2022 13:04:27 +0200 Subject: [PATCH 028/138] Strip invalid XML characters from response --- src/pylast/__init__.py | 24 ++++++++++++++++++++---- tests/unicode_test.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 54d4d40..e32e849 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -24,6 +24,7 @@ import hashlib import html.entities import logging import os +import re import shelve import ssl import tempfile @@ -969,7 +970,7 @@ class _Request: conn.close() return response_text - def execute(self, cacheable=False): + def execute(self, cacheable: bool = False) -> xml.dom.minidom.Document: """Returns the XML DOM response of the POST Request from the server""" if self.network.is_caching_enabled() and cacheable: @@ -977,13 +978,12 @@ class _Request: else: response = self._download_response() - return minidom.parseString(_string(response).replace("opensearch:", "")) + return _parse_response(response) def _check_response_for_errors(self, response): """Checks the response for errors and raises one if any exists.""" - try: - doc = minidom.parseString(_string(response).replace("opensearch:", "")) + doc = _parse_response(response) except Exception as e: raise MalformedResponseError(self.network, e) from e @@ -2950,4 +2950,20 @@ def _unescape_htmlentity(string): return string +def _parse_response(response: str) -> xml.dom.minidom.Document: + response = _string(response).replace("opensearch:", "") + try: + doc = minidom.parseString(response) + except xml.parsers.expat.ExpatError: + # Try again. For performance, we only remove when needed in rare cases. + doc = minidom.parseString(_remove_invalid_xml_chars(response)) + return doc + + +def _remove_invalid_xml_chars(string: str) -> str: + return re.sub( + r"[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u10000-\u10FFF]+", "", string + ) + + # End of file diff --git a/tests/unicode_test.py b/tests/unicode_test.py index 7b3c271..350256c 100644 --- a/tests/unicode_test.py +++ b/tests/unicode_test.py @@ -27,3 +27,42 @@ def test_get_cache_key(artist): def test_cast_and_hash(obj): assert type(str(obj)) is str assert isinstance(hash(obj), int) + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + # Plain text + 'test album name', + 'test album name', + ), + ( + # Contains Unicode ENQ Enquiry control character + 'test album \u0005name', + 'test album name', + ), + ], +) +def test__remove_invalid_xml_chars(test_input: str, expected: str) -> None: + assert pylast._remove_invalid_xml_chars(test_input) == expected + + +@pytest.mark.parametrize( + "test_input, expected", + [ + ( + # Plain text + 'test album name', + 'test album name', + ), + ( + # Contains Unicode ENQ Enquiry control character + 'test album \u0005name', + 'test album name', + ), + ], +) +def test__parse_response(test_input: str, expected: str) -> None: + doc = pylast._parse_response(test_input) + assert doc.toxml() == expected From c63e0a75ef4e3a5ed450f2957722a8e09a615b92 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 18:02:15 +0200 Subject: [PATCH 029/138] Restore support for Python 3.6 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 6 +++--- setup.cfg | 3 ++- tox.ini | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f133aeb..ba3aaf0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10"] + python-version: ["pypy-3.8", "3.6", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-20.04] include: # Include new variables for Codecov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4f14266..c1ee091 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,19 +3,19 @@ repos: rev: v2.31.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py36-plus] - repo: https://github.com/psf/black rev: 21.12b0 hooks: - id: black - args: [--target-version=py37] + args: [--target-version=py36] - repo: https://github.com/asottile/blacken-docs rev: v1.12.0 hooks: - id: blacken-docs - args: [--target-version=py37] + args: [--target-version=py36] additional_dependencies: [black==21.11b1] - repo: https://github.com/PyCQA/isort diff --git a/setup.cfg b/setup.cfg index 5a80546..7b806d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,7 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -33,7 +34,7 @@ keywords = packages = find: install_requires = importlib-metadata;python_version < '3.8' -python_requires = >=3.7 +python_requires = >=3.6 package_dir = =src setup_requires = setuptools-scm diff --git a/tox.ini b/tox.ini index 9022a23..c04ab95 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = lint - py{py3, 310, 39, 38, 37} + py{py3, 310, 39, 38, 37, 36} [testenv] passenv = From 3b7cb9c8c7d7f79f9a4b426416169f4a81b213c1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 19:04:38 +0200 Subject: [PATCH 030/138] Deprecate is_streamable and is_fulltrack_available --- src/pylast/__init__.py | 50 +++++++++++++++++++++++++++++------------- tests/test_artist.py | 3 ++- tests/test_track.py | 6 +++-- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index e32e849..d22bc3a 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -29,6 +29,7 @@ import shelve import ssl import tempfile import time +import warnings import xml.dom from http.client import HTTPSConnection from urllib.parse import quote_plus @@ -1783,13 +1784,18 @@ class Artist(_Taggable): return self.listener_count def is_streamable(self): - """Returns True if the artist is streamable.""" - - return bool( - _number( - _extract(self._request(self.ws_prefix + ".getInfo", True), "streamable") - ) + """Returns True if the artist is streamable: always False because Last.fm has + deprecated the Radio API.""" + warnings.warn( + "Always returns False. Last.fm has deprecated the Radio API and will " + "it at some point. is_streamable() will be removed in pylast 5.0.0. " + "See https://www.last.fm/api/radio and " + "https://support.last.fm/t/" + "is-the-streamable-attribute-broken-it-always-returns-0/39723/3", + DeprecationWarning, + stacklevel=2, ) + return False def get_bio(self, section, language=None): """ @@ -2130,18 +2136,32 @@ class Track(_Opus): return bool(loved) def is_streamable(self): - """Returns True if the track is available at Last.fm.""" - - doc = self._request(self.ws_prefix + ".getInfo", True) - return _extract(doc, "streamable") == "1" + """Returns True if the artist is streamable: always False because Last.fm has + deprecated the Radio API.""" + warnings.warn( + "Always returns False. Last.fm has deprecated the Radio API and will " + "it at some point. is_streamable() will be removed in pylast 5.0.0. " + "See https://www.last.fm/api/radio and " + "https://support.last.fm/t/" + "is-the-streamable-attribute-broken-it-always-returns-0/39723/3", + DeprecationWarning, + stacklevel=2, + ) + return False def is_fulltrack_available(self): - """Returns True if the full track is available for streaming.""" - - doc = self._request(self.ws_prefix + ".getInfo", True) - return ( - doc.getElementsByTagName("streamable")[0].getAttribute("fulltrack") == "1" + """Returns True if the full track is available for streaming: always False + because Last.fm has deprecated the Radio API.""" + warnings.warn( + "Always returns False. Last.fm has deprecated the Radio API and will " + "remove it at some point. is_fulltrack_available() will be removed in " + "pylast 5.0.0. See https://www.last.fm/api/radio and " + "https://support.last.fm/t/" + "is-the-streamable-attribute-broken-it-always-returns-0/39723/3", + DeprecationWarning, + stacklevel=2, ) + return False def get_album(self): """Returns the album object of this track.""" diff --git a/tests/test_artist.py b/tests/test_artist.py index a911882..463af69 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -229,7 +229,8 @@ class TestPyLastArtist(TestPyLastWithLastFm): mbid = artist1.get_mbid() playcount = artist1.get_playcount() - streamable = artist1.is_streamable() + with pytest.warns(DeprecationWarning): + streamable = artist1.is_streamable() name = artist1.get_name(properly_capitalized=False) name_cap = artist1.get_name(properly_capitalized=True) diff --git a/tests/test_track.py b/tests/test_track.py index b56c018..e7ea5e3 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -123,7 +123,8 @@ class TestPyLastTrack(TestPyLastWithLastFm): track = pylast.Track("Nirvana", "Lithium", self.network) # Act - streamable = track.is_streamable() + with pytest.warns(DeprecationWarning): + streamable = track.is_streamable() # Assert assert not streamable @@ -133,7 +134,8 @@ class TestPyLastTrack(TestPyLastWithLastFm): track = pylast.Track("Nirvana", "Lithium", self.network) # Act - fulltrack_available = track.is_fulltrack_available() + with pytest.warns(DeprecationWarning): + fulltrack_available = track.is_fulltrack_available() # Assert assert not fulltrack_available From d672e89f233424eb564f127c260aafaee7714ce7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 21:15:11 +0200 Subject: [PATCH 031/138] Is an xfail passing unexpectedly? Make it fail --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index 34667c8..3f83bd3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,3 +2,5 @@ filterwarnings = once::DeprecationWarning once::PendingDeprecationWarning + +xfail_strict=true From 1841fb66dc9c2b15a803c0c56f4bb9c50907aeb3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 22:07:03 +0200 Subject: [PATCH 032/138] This test now passes, although some other MBID searches are still broken --- tests/test_network.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_network.py b/tests/test_network.py index 2ed839f..2f743ab 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -267,8 +267,6 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert isinstance(artist, pylast.Artist) assert artist.name in ("MusicBrainz Test Artist", "MusicBrainzz Test Artist") - @pytest.mark.xfail(reason="Broken at Last.fm: Track not found") - # https://support.last.fm/t/track-getinfo-with-mbid-returns-6-track-not-found/47905 def test_track_mbid(self): # Arrange mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0" From 3ffe7cf65ac4336322958367d284f158c0c6c104 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 22:26:16 +0200 Subject: [PATCH 033/138] test_get_userplaycount now passes --- tests/test_artist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_artist.py b/tests/test_artist.py index 463af69..8cc4f85 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -268,7 +268,6 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Assert assert corrected_artist_name == "Guns N' Roses" - @pytest.mark.xfail def test_get_userplaycount(self): # Arrange artist = pylast.Artist("John Lennon", self.network, username=self.username) @@ -277,4 +276,4 @@ class TestPyLastArtist(TestPyLastWithLastFm): playcount = artist.get_userplaycount() # Assert - assert playcount >= 0 # whilst xfail: # pragma: no cover + assert playcount >= 0 From b151dd0c9322783470a4a1772a0cef69c74fc733 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 22:38:33 +0200 Subject: [PATCH 034/138] Remove Mergify, use native GitHub auto-merge instead --- .github/labels.yml | 3 --- .mergify.yml | 8 -------- 2 files changed, 11 deletions(-) delete mode 100644 .mergify.yml diff --git a/.github/labels.yml b/.github/labels.yml index 38b5fdb..090914a 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -91,9 +91,6 @@ - color: b60205 description: Removal of a feature, usually done in major releases name: removal -- color: 2d18b2 - description: "To automatically merge PRs that are ready" - name: automerge - color: 0366d6 description: "For dependencies" name: dependencies diff --git a/.mergify.yml b/.mergify.yml deleted file mode 100644 index dad8639..0000000 --- a/.mergify.yml +++ /dev/null @@ -1,8 +0,0 @@ -pull_request_rules: - - name: Automatic merge on approval - conditions: - - label=automerge - - status-success=build - actions: - merge: - method: merge From 6465f4cf51f61bab12fd470c9fe95fec3e2b23e9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 31 Jan 2022 12:50:28 +0200 Subject: [PATCH 035/138] Update link to deploy action --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index 452bca6..9a4d1bc 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -12,7 +12,7 @@ - [ ] Publish release -- [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions?query=workflow%3ADeploy) +- [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions/workflows/deploy.yml) has deployed to [PyPI](https://pypi.org/project/pylast/#history) - [ ] Check installation: From d610721167f372bb426dc9b921712fdd6768509a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 25 Jan 2022 00:42:03 +0200 Subject: [PATCH 036/138] Drop support for Python EOL 3.6 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 6 +++--- setup.cfg | 3 +-- tox.ini | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba3aaf0..f133aeb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-20.04] include: # Include new variables for Codecov diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1ee091..4f14266 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,19 +3,19 @@ repos: rev: v2.31.0 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py37-plus] - repo: https://github.com/psf/black rev: 21.12b0 hooks: - id: black - args: [--target-version=py36] + args: [--target-version=py37] - repo: https://github.com/asottile/blacken-docs rev: v1.12.0 hooks: - id: blacken-docs - args: [--target-version=py36] + args: [--target-version=py37] additional_dependencies: [black==21.11b1] - repo: https://github.com/PyCQA/isort diff --git a/setup.cfg b/setup.cfg index 7b806d9..5a80546 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,6 @@ classifiers = License :: OSI Approved :: Apache Software License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -34,7 +33,7 @@ keywords = packages = find: install_requires = importlib-metadata;python_version < '3.8' -python_requires = >=3.6 +python_requires = >=3.7 package_dir = =src setup_requires = setuptools-scm diff --git a/tox.ini b/tox.ini index c04ab95..9022a23 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = lint - py{py3, 310, 39, 38, 37, 36} + py{py3, 310, 39, 38, 37} [testenv] passenv = From bb05699252b55f1cc6ccda34487b40920b51645e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 31 Jan 2022 12:56:38 +0200 Subject: [PATCH 037/138] Remove deprecated is_streamable and is_fulltrack_available --- src/pylast/__init__.py | 43 ------------------------------------------ tests/test_artist.py | 3 --- tests/test_track.py | 22 --------------------- 3 files changed, 68 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index d22bc3a..dfc0b1c 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -29,7 +29,6 @@ import shelve import ssl import tempfile import time -import warnings import xml.dom from http.client import HTTPSConnection from urllib.parse import quote_plus @@ -1783,20 +1782,6 @@ class Artist(_Taggable): ) return self.listener_count - def is_streamable(self): - """Returns True if the artist is streamable: always False because Last.fm has - deprecated the Radio API.""" - warnings.warn( - "Always returns False. Last.fm has deprecated the Radio API and will " - "it at some point. is_streamable() will be removed in pylast 5.0.0. " - "See https://www.last.fm/api/radio and " - "https://support.last.fm/t/" - "is-the-streamable-attribute-broken-it-always-returns-0/39723/3", - DeprecationWarning, - stacklevel=2, - ) - return False - def get_bio(self, section, language=None): """ Returns a section of the bio. @@ -2135,34 +2120,6 @@ class Track(_Opus): loved = _number(_extract(doc, "userloved")) return bool(loved) - def is_streamable(self): - """Returns True if the artist is streamable: always False because Last.fm has - deprecated the Radio API.""" - warnings.warn( - "Always returns False. Last.fm has deprecated the Radio API and will " - "it at some point. is_streamable() will be removed in pylast 5.0.0. " - "See https://www.last.fm/api/radio and " - "https://support.last.fm/t/" - "is-the-streamable-attribute-broken-it-always-returns-0/39723/3", - DeprecationWarning, - stacklevel=2, - ) - return False - - def is_fulltrack_available(self): - """Returns True if the full track is available for streaming: always False - because Last.fm has deprecated the Radio API.""" - warnings.warn( - "Always returns False. Last.fm has deprecated the Radio API and will " - "remove it at some point. is_fulltrack_available() will be removed in " - "pylast 5.0.0. See https://www.last.fm/api/radio and " - "https://support.last.fm/t/" - "is-the-streamable-attribute-broken-it-always-returns-0/39723/3", - DeprecationWarning, - stacklevel=2, - ) - return False - def get_album(self): """Returns the album object of this track.""" if "album" in self.info and self.info["album"] is not None: diff --git a/tests/test_artist.py b/tests/test_artist.py index 8cc4f85..44bacfd 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -229,8 +229,6 @@ class TestPyLastArtist(TestPyLastWithLastFm): mbid = artist1.get_mbid() playcount = artist1.get_playcount() - with pytest.warns(DeprecationWarning): - streamable = artist1.is_streamable() name = artist1.get_name(properly_capitalized=False) name_cap = artist1.get_name(properly_capitalized=True) @@ -240,7 +238,6 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert name.lower() == name_cap.lower() assert url == "https://www.last.fm/music/radiohead" assert mbid == "a74b1b7f-71a5-4011-9441-d0b5e4122711" - assert isinstance(streamable, bool) def test_artist_eq_none_is_false(self): # Arrange diff --git a/tests/test_track.py b/tests/test_track.py index e7ea5e3..00f4eca 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -118,28 +118,6 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert assert duration >= 200000 - def test_track_is_streamable(self): - # Arrange - track = pylast.Track("Nirvana", "Lithium", self.network) - - # Act - with pytest.warns(DeprecationWarning): - streamable = track.is_streamable() - - # Assert - assert not streamable - - def test_track_is_fulltrack_available(self): - # Arrange - track = pylast.Track("Nirvana", "Lithium", self.network) - - # Act - with pytest.warns(DeprecationWarning): - fulltrack_available = track.is_fulltrack_available() - - # Assert - assert not fulltrack_available - def test_track_get_album(self): # Arrange track = pylast.Track("Nirvana", "Lithium", self.network) From 44ade40579610340bd0eac51e81f0a0be9055e98 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 21 Nov 2021 23:47:01 +0200 Subject: [PATCH 038/138] Replace http.client with HTTPX --- setup.cfg | 1 + src/pylast/__init__.py | 84 ++++++++++++++++-------------------------- tests/test_network.py | 5 +-- 3 files changed, 35 insertions(+), 55 deletions(-) diff --git a/setup.cfg b/setup.cfg index 5a80546..a48f245 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ keywords = [options] packages = find: install_requires = + httpx importlib-metadata;python_version < '3.8' python_requires = >=3.7 package_dir = =src diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index dfc0b1c..910f847 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -30,10 +30,11 @@ import ssl import tempfile import time import xml.dom -from http.client import HTTPSConnection from urllib.parse import quote_plus from xml.dom import Node, minidom +import httpx + try: # Python 3.8+ import importlib.metadata as importlib_metadata @@ -125,6 +126,12 @@ DELAY_TIME = 0.2 # Python >3.4 has sane defaults SSL_CONTEXT = ssl.create_default_context() +HEADERS = { + "Content-type": "application/x-www-form-urlencoded", + "Accept-Charset": "utf-8", + "User-Agent": f"pylast/{__version__}", +} + logger = logging.getLogger(__name__) logging.getLogger(__name__).addHandler(logging.NullHandler()) @@ -390,7 +397,7 @@ class _Network: def enable_proxy(self, host, port): """Enable a default web proxy""" - self.proxy = [host, _number(port)] + self.proxy = f"{host}:{_number(port)}" self.proxy_enabled = True def disable_proxy(self): @@ -906,68 +913,41 @@ class _Request: self.network._delay_call() username = self.params.pop("username", None) - username = f"?username={username}" if username is not None else "" - - data = [] - for name in self.params.keys(): - data.append("=".join((name, quote_plus(_string(self.params[name]))))) - data = "&".join(data) - - headers = { - "Content-type": "application/x-www-form-urlencoded", - "Accept-Charset": "utf-8", - "User-Agent": "pylast/" + __version__, - } + username = "" if username is None else f"?username={username}" (host_name, host_subdir) = self.network.ws_server if self.network.is_proxy_enabled(): - conn = HTTPSConnection( - context=SSL_CONTEXT, - host=self.network._get_proxy()[0], - port=self.network._get_proxy()[1], + client = httpx.Client( + verify=SSL_CONTEXT, + base_url=f"https://{host_name}", + headers=HEADERS, + proxies=f"http://{self.network._get_proxy()}", + ) + else: + client = httpx.Client( + verify=SSL_CONTEXT, + base_url=f"https://{host_name}", + headers=HEADERS, ) - try: - conn.request( - method="POST", - url=f"https://{host_name}{host_subdir}{username}", - body=data, - headers=headers, - ) - except Exception as e: - raise NetworkError(self.network, e) from e - - else: - conn = HTTPSConnection(context=SSL_CONTEXT, host=host_name) - - try: - conn.request( - method="POST", - url=f"{host_subdir}{username}", - body=data, - headers=headers, - ) - except Exception as e: - raise NetworkError(self.network, e) from e - try: - response = conn.getresponse() - if response.status in [500, 502, 503, 504]: - raise WSError( - self.network, - response.status, - "Connection to the API failed with HTTP code " - + str(response.status), - ) - response_text = _unicode(response.read()) + response = client.post(f"{host_subdir}{username}", data=self.params) except Exception as e: - raise MalformedResponseError(self.network, e) from e + raise NetworkError(self.network, e) from e + + if response.status_code in (500, 502, 503, 504): + raise WSError( + self.network, + response.status_code, + f"Connection to the API failed with HTTP code {response.status_code}", + ) + response_text = _unicode(response.read()) try: self._check_response_for_errors(response_text) finally: - conn.close() + client.close() return response_text def execute(self, cacheable: bool = False) -> xml.dom.minidom.Document: diff --git a/tests/test_network.py b/tests/test_network.py index 2f743ab..ad141f2 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Integration (not unit) tests for pylast.py """ @@ -297,13 +296,13 @@ class TestPyLastNetwork(TestPyLastWithLastFm): def test_proxy(self): # Arrange - host = "https://example.com" + host = "example.com" port = 1234 # Act / Assert self.network.enable_proxy(host, port) assert self.network.is_proxy_enabled() - assert self.network._get_proxy() == ["https://example.com", 1234] + assert self.network._get_proxy() == "example.com:1234" self.network.disable_proxy() assert not self.network.is_proxy_enabled() From 122c870312666a13809d3ad94db2501b0c342d77 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 11 Jan 2022 13:41:26 +0200 Subject: [PATCH 039/138] Replace _string with str --- src/pylast/__init__.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 910f847..3f00b5a 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -1101,7 +1101,7 @@ Image = collections.namedtuple( def _string_output(func): def r(*args): - return _string(func(*args)) + return str(func(*args)) return r @@ -2730,12 +2730,6 @@ def _unicode(text): return str(text) -def _string(string): - if isinstance(string, str): - return string - return str(string) - - def cleanup_nodes(doc): """ Remove text nodes containing only whitespace @@ -2881,7 +2875,7 @@ def _extract_tracks(doc, network): def _url_safe(text): """Does all kinds of tricks on a text to make it safe to use in a URL.""" - return quote_plus(quote_plus(_string(text))).lower() + return quote_plus(quote_plus(str(text))).lower() def _number(string): @@ -2908,7 +2902,7 @@ def _unescape_htmlentity(string): def _parse_response(response: str) -> xml.dom.minidom.Document: - response = _string(response).replace("opensearch:", "") + response = str(response).replace("opensearch:", "") try: doc = minidom.parseString(response) except xml.parsers.expat.ExpatError: From a418f64b15600ebfe2cd6234de833f97fd5ce1a4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 11 Jan 2022 13:44:19 +0200 Subject: [PATCH 040/138] Simplify _unicode --- src/pylast/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 3f00b5a..c1e3fa0 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -2724,8 +2724,6 @@ def md5(text): def _unicode(text): if isinstance(text, bytes): return str(text, "utf-8") - elif isinstance(text, str): - return text else: return str(text) From da2e7152ba36b49bb9cadbae173194749a07f50c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 15 Jan 2022 18:46:23 +0200 Subject: [PATCH 041/138] Update blacken-docs to match main black --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4f14266..23bc55c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: blacken-docs args: [--target-version=py37] - additional_dependencies: [black==21.11b1] + additional_dependencies: [black==21.12b0] - repo: https://github.com/PyCQA/isort rev: 5.10.1 From 1a45c3b91914fc4477156d9dfc4a32e68f573179 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 17:23:08 +0200 Subject: [PATCH 042/138] Allow setting multiple proxies + some cleanup --- src/pylast/__init__.py | 33 ++++++++++++++------------------- tests/test_network.py | 7 +++---- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index c1e3fa0..1360889 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -19,6 +19,8 @@ # # https://github.com/pylast/pylast +from __future__ import annotations + import collections import hashlib import html.entities @@ -194,7 +196,6 @@ class _Network: self.urls = urls self.cache_backend = None - self.proxy_enabled = False self.proxy = None self.last_call_time = 0 self.limit_rate = False @@ -394,26 +395,20 @@ class _Network: return seq - def enable_proxy(self, host, port): - """Enable a default web proxy""" + def enable_proxy(self, proxy: str | dict) -> None: + """Enable default web proxy. + Multiple proxies can be passed as a `dict`, see + https://www.python-httpx.org/advanced/#http-proxying + """ + self.proxy = proxy - self.proxy = f"{host}:{_number(port)}" - self.proxy_enabled = True - - def disable_proxy(self): + def disable_proxy(self) -> None: """Disable using the web proxy""" + self.proxy = None - self.proxy_enabled = False - - def is_proxy_enabled(self): - """Returns True if a web proxy is enabled.""" - - return self.proxy_enabled - - def _get_proxy(self): - """Returns proxy details.""" - - return self.proxy + def is_proxy_enabled(self) -> bool: + """Returns True if web proxy is enabled.""" + return self.proxy is not None def enable_rate_limit(self): """Enables rate limiting for this network""" @@ -922,7 +917,7 @@ class _Request: verify=SSL_CONTEXT, base_url=f"https://{host_name}", headers=HEADERS, - proxies=f"http://{self.network._get_proxy()}", + proxies=self.network.proxy, ) else: client = httpx.Client( diff --git a/tests/test_network.py b/tests/test_network.py index ad141f2..8937c53 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -296,13 +296,12 @@ class TestPyLastNetwork(TestPyLastWithLastFm): def test_proxy(self): # Arrange - host = "example.com" - port = 1234 + proxy = "http://example.com:1234" # Act / Assert - self.network.enable_proxy(host, port) + self.network.enable_proxy(proxy) assert self.network.is_proxy_enabled() - assert self.network._get_proxy() == "example.com:1234" + assert self.network.proxy == "http://example.com:1234" self.network.disable_proxy() assert not self.network.is_proxy_enabled() From f7090f26a0dfcbe7a46ecbf285480a3a6a284d51 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 27 Feb 2022 16:38:08 +0200 Subject: [PATCH 043/138] Output coverage XML for Codecov to upload --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9022a23..48e8339 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ passenv = extras = tests commands = - pytest -v -s -W all --cov pylast --cov tests --cov-report term-missing --random-order {posargs} + pytest -v -s -W all --cov pylast --cov tests --cov-report term-missing --cov-report xml --random-order {posargs} [testenv:lint] passenv = From fe7484b3cac5b6d9c26742388548d054683a1aa5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 27 Feb 2022 16:30:09 +0200 Subject: [PATCH 044/138] If album has no MBID, album.get_getmbid() returns None --- src/pylast/__init__.py | 5 ++--- tests/test_album.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 1360889..cfbe52f 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -18,7 +18,6 @@ # limitations under the License. # # https://github.com/pylast/pylast - from __future__ import annotations import collections @@ -1595,7 +1594,7 @@ class _Opus(_Taggable): ) ) - def get_mbid(self): + def get_mbid(self) -> str | None: """Returns the MusicBrainz ID of the album or track.""" doc = self._request(self.ws_prefix + ".getInfo", cacheable=True) @@ -1604,7 +1603,7 @@ class _Opus(_Taggable): lfm = doc.getElementsByTagName("lfm")[0] opus = next(self._get_children_by_tag_name(lfm, self.ws_prefix)) mbid = next(self._get_children_by_tag_name(opus, "mbid")) - return mbid.firstChild.nodeValue + return mbid.firstChild.nodeValue if mbid.firstChild else None except StopIteration: return None diff --git a/tests/test_album.py b/tests/test_album.py index e3ca4f7..56c469b 100755 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -96,3 +96,23 @@ class TestPyLastAlbum(TestPyLastWithLastFm): # Assert self.assert_startswith(image, "https://") self.assert_endswith(image, ".gif") + + def test_mbid(self): + # Arrange + album = self.network.get_album("Radiohead", "OK Computer") + + # Act + mbid = album.get_mbid() + + # Assert + assert mbid == "0b6b4ba0-d36f-47bd-b4ea-6a5b91842d29" + + def test_no_mbid(self): + # Arrange + album = self.network.get_album("Test Artist", "Test Album") + + # Act + mbid = album.get_mbid() + + # Assert + assert mbid is None From b726227d5d5603e1182705e08ff0e8de29cacf7d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 1 Mar 2022 11:48:22 +0200 Subject: [PATCH 045/138] Upgrade to actions/setup-python@v3 Committed via https://github.com/asottile/all-repos --- .github/workflows/deploy.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 627be8b..5bd3973 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "3.10" cache: pip diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 769ea4e..d9014a8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,5 +8,5 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v3 - uses: pre-commit/action@v2.0.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f133aeb..c9dc716 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} cache: pip From dd8836e59b8806d5a78818a9bcad0844d4d0c84c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 3 Mar 2022 13:15:26 +0200 Subject: [PATCH 046/138] Logging: log method names at INFO level, also log API return data at DEBUG level --- README.md | 8 ++++++-- src/pylast/__init__.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1232eb1..1a2a667 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,8 @@ To enable from your own code: import logging import pylast -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.INFO) + network = pylast.LastFMNetwork(...) ``` @@ -152,5 +153,8 @@ network = pylast.LastFMNetwork(...) To enable from pytest: ```sh -pytest --log-cli-level debug -k test_album_search_images +pytest --log-cli-level info -k test_album_search_images ``` + +To also see data returned from the API, use `level=logging.DEBUG` or +`--log-cli-level debug` instead. diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index cfbe52f..3065e7d 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -822,7 +822,7 @@ class _Request: """Representing an abstract web service operation.""" def __init__(self, network, method_name, params=None): - logger.debug(method_name) + logger.info(method_name) if params is None: params = {} @@ -962,7 +962,7 @@ class _Request: raise MalformedResponseError(self.network, e) from e e = doc.getElementsByTagName("lfm")[0] - # logger.debug(doc.toprettyxml()) + logger.debug(doc.toprettyxml()) if e.getAttribute("status") != "ok": e = doc.getElementsByTagName("error")[0] From 5f8d150652dbbf4f3dd4636bc5655824d16430fa Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 17:34:20 +0200 Subject: [PATCH 047/138] Remove redundant _get_cache_backend and add some typing --- src/pylast/__init__.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 3065e7d..2077e0e 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -409,44 +409,37 @@ class _Network: """Returns True if web proxy is enabled.""" return self.proxy is not None - def enable_rate_limit(self): + def enable_rate_limit(self) -> None: """Enables rate limiting for this network""" self.limit_rate = True - def disable_rate_limit(self): + def disable_rate_limit(self) -> None: """Disables rate limiting for this network""" self.limit_rate = False - def is_rate_limited(self): + def is_rate_limited(self) -> bool: """Return True if web service calls are rate limited""" return self.limit_rate - def enable_caching(self, file_path=None): + def enable_caching(self, file_path=None) -> None: """Enables caching request-wide for all cacheable calls. * file_path: A file path for the backend storage file. If None set, a temp file would probably be created, according the backend. """ - if not file_path: self.cache_backend = _ShelfCacheBackend.create_shelf() return self.cache_backend = _ShelfCacheBackend(file_path) - def disable_caching(self): + def disable_caching(self) -> None: """Disables all caching features.""" - self.cache_backend = None - def is_caching_enabled(self): + def is_caching_enabled(self) -> bool: """Returns True if caching is enabled.""" - - return not (self.cache_backend is None) - - def _get_cache_backend(self): - - return self.cache_backend + return self.cache_backend is not None def search_for_album(self, album_name): """Searches for an album by its name. Returns a AlbumSearch object. @@ -839,7 +832,7 @@ class _Request: self.params["method"] = method_name if network.is_caching_enabled(): - self.cache = network._get_cache_backend() + self.cache = network.cache_backend if self.session_key: self.params["sk"] = self.session_key From b373de6c68b2577e48d94b3baee557c0a2061d6d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jan 2022 23:27:58 +0200 Subject: [PATCH 048/138] More f-strings --- src/pylast/__init__.py | 73 ++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 2077e0e..36ee0e9 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -216,7 +216,7 @@ class _Network: self.session_key = sk_gen.get_session_key(self.username, self.password_hash) def __str__(self): - return "%s Network" % self.name + return f"{self.name} Network" def get_artist(self, artist_name): """ @@ -276,9 +276,7 @@ class _Network: return self.domain_names[domain_language] def _get_url(self, domain, url_type): - return "https://{}/{}".format( - self._get_language_domain(domain), self.urls[url_type] - ) + return f"https://{self._get_language_domain(domain)}/{self.urls[url_type]}" def _get_ws_auth(self): """ @@ -601,8 +599,8 @@ class _Network: params = {} for i in range(len(tracks_to_scrobble)): - params["artist[%d]" % i] = tracks_to_scrobble[i]["artist"] - params["track[%d]" % i] = tracks_to_scrobble[i]["title"] + params[f"artist[{i}]"] = tracks_to_scrobble[i]["artist"] + params[f"track[{i}]"] = tracks_to_scrobble[i]["title"] additional_args = ( "timestamp", @@ -628,7 +626,7 @@ class _Network: else: maps_to = arg - params["%s[%d]" % (maps_to, i)] = tracks_to_scrobble[i][arg] + params[f"{maps_to}[{i}]"] = tracks_to_scrobble[i][arg] _Request(self, "track.scrobble", params).execute() @@ -702,16 +700,14 @@ class LastFMNetwork(_Network): ) def __repr__(self): - return "pylast.LastFMNetwork(%s)" % ( - ", ".join( - ( - "'%s'" % self.api_key, - "'%s'" % self.api_secret, - "'%s'" % self.session_key, - "'%s'" % self.username, - "'%s'" % self.password_hash, - ) - ) + return ( + "pylast.LastFMNetwork(" + f"'{self.api_key}', " + f"'{self.api_secret}', " + f"'{self.session_key}', " + f"'{self.username}', " + f"'{self.password_hash}'" + ")" ) @@ -768,16 +764,14 @@ class LibreFMNetwork(_Network): ) def __repr__(self): - return "pylast.LibreFMNetwork(%s)" % ( - ", ".join( - ( - "'%s'" % self.api_key, - "'%s'" % self.api_secret, - "'%s'" % self.session_key, - "'%s'" % self.username, - "'%s'" % self.password_hash, - ) - ) + return ( + "pylast.LibreFMNetwork(" + f"'{self.api_key}', " + f"'{self.api_secret}', " + f"'{self.session_key}', " + f"'{self.username}', " + f"'{self.password_hash}'" + ")" ) @@ -1018,8 +1012,10 @@ class SessionKeyGenerator: token = self._get_web_auth_token() - url = "{homepage}/api/auth/?api_key={api}&token={token}".format( - homepage=self.network.homepage, api=self.network.api_key, token=token + url = ( + f"{self.network.homepage}/api/auth/" + f"?api_key={self.network.api_key}" + f"&token={token}" ) self.web_auth_tokens[url] = token @@ -1443,8 +1439,9 @@ class MalformedResponseError(PyLastError): self.underlying_error = underlying_error def __str__(self): - return "Malformed response from {}. Underlying error: {}".format( - self.network.name, str(self.underlying_error) + return ( + f"Malformed response from {self.network.name}. " + f"Underlying error: {self.underlying_error}" ) @@ -1456,7 +1453,7 @@ class NetworkError(PyLastError): self.underlying_error = underlying_error def __str__(self): - return "NetworkError: %s" % str(self.underlying_error) + return f"NetworkError: {self.underlying_error}" class _Opus(_Taggable): @@ -1494,16 +1491,14 @@ class _Opus(_Taggable): self.info = info def __repr__(self): - return "pylast.{}({}, {}, {})".format( - self.ws_prefix.title(), - repr(self.artist.name), - repr(self.title), - repr(self.network), + return ( + f"pylast.{self.ws_prefix.title()}" + f"({repr(self.artist.name)}, {repr(self.title)}, {repr(self.network)})" ) @_string_output def __str__(self): - return _unicode("%s - %s") % (self.get_artist().get_name(), self.get_title()) + return f"{self.get_artist().get_name()} - {self.get_title()}" def __eq__(self, other): if type(self) != type(other): @@ -2881,7 +2876,7 @@ def _number(string): def _unescape_htmlentity(string): mapping = html.entities.name2codepoint for key in mapping: - string = string.replace("&%s;" % key, chr(mapping[key])) + string = string.replace(f"&{key};", chr(mapping[key])) return string From 549437b640a2190b78615549bb421fd24ce5522d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 27 Feb 2022 20:49:58 +0200 Subject: [PATCH 049/138] Fix 'a a...' to 'an a...' --- src/pylast/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 36ee0e9..476bca7 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -440,13 +440,13 @@ class _Network: return self.cache_backend is not None def search_for_album(self, album_name): - """Searches for an album by its name. Returns a AlbumSearch object. + """Searches for an album by its name. Returns an AlbumSearch object. Use get_next_page() to retrieve sequences of results.""" return AlbumSearch(album_name, self) def search_for_artist(self, artist_name): - """Searches of an artist by its name. Returns a ArtistSearch object. + """Searches of an artist by its name. Returns an ArtistSearch object. Use get_next_page() to retrieve sequences of results.""" return ArtistSearch(artist_name, self) @@ -976,7 +976,7 @@ class SessionKeyGenerator: A session key's lifetime is infinite, unless the user revokes the rights of the given API Key. - If you create a Network object with just a API_KEY and API_SECRET and a + If you create a Network object with just an API_KEY and API_SECRET and a username and a password_hash, a SESSION_KEY will be automatically generated for that network and stored in it so you don't have to do this manually, unless you want to. From 95c8b1656489efa301c3a181ad50c0baecdca7e2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:20:30 +0300 Subject: [PATCH 050/138] Upgrade Black to fix Click --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23bc55c..9fd1e6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,18 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.3.0 hooks: - id: black args: [--target-version=py37] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.0 + rev: v1.12.1 hooks: - id: blacken-docs args: [--target-version=py37] @@ -41,7 +41,7 @@ repos: - id: check-yaml - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 + rev: v1.20.1 hooks: - id: setup-cfg-fmt From b0f2f5fe1355767686e7233c7417b6b21f0a27cc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:20:48 +0300 Subject: [PATCH 051/138] For some reason the earlier track is returning duration=0 --- tests/test_track.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_track.py b/tests/test_track.py index 00f4eca..18f1d87 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -110,7 +110,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): def test_track_get_duration(self): # Arrange - track = pylast.Track("Nirvana", "Lithium", self.network) + track = pylast.Track("Cher", "Believe", self.network) # Act duration = track.get_duration() From 4e5fe31572a34e94c8e8886c7fee495cf362c60a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:38:16 +0300 Subject: [PATCH 052/138] Rename variable e to element --- src/pylast/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 476bca7..51c97f2 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -948,13 +948,13 @@ class _Request: except Exception as e: raise MalformedResponseError(self.network, e) from e - e = doc.getElementsByTagName("lfm")[0] + element = doc.getElementsByTagName("lfm")[0] logger.debug(doc.toprettyxml()) - if e.getAttribute("status") != "ok": - e = doc.getElementsByTagName("error")[0] - status = e.getAttribute("code") - details = e.firstChild.data.strip() + if element.getAttribute("status") != "ok": + element = doc.getElementsByTagName("error")[0] + status = element.getAttribute("code") + details = element.firstChild.data.strip() raise WSError(self.network, status, details) From 6c3f3afb3a2dc7f8a8e736dfeb8df77ef5764e83 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:43:37 +0300 Subject: [PATCH 053/138] autotyping: --none-return: add a -> None return type to functions without any return, yield, or raise in their body --- src/pylast/__init__.py | 78 ++++++++++++++++++++++-------------------- tests/test_album.py | 22 ++++++------ tests/test_artist.py | 40 +++++++++++----------- tests/test_country.py | 4 +-- tests/test_library.py | 10 +++--- tests/test_librefm.py | 4 +-- tests/test_network.py | 60 ++++++++++++++++---------------- tests/test_pylast.py | 20 +++++------ tests/test_tag.py | 8 ++--- tests/test_track.py | 38 ++++++++++---------- tests/test_user.py | 78 +++++++++++++++++++++--------------------- tests/unicode_test.py | 4 +-- 12 files changed, 184 insertions(+), 182 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 51c97f2..231576f 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -156,7 +156,7 @@ class _Network: domain_names, urls, token=None, - ): + ) -> None: """ name: the name of the network homepage: the homepage URL @@ -284,7 +284,7 @@ class _Network: """ return self.api_key, self.api_secret, self.session_key - def _delay_call(self): + def _delay_call(self) -> None: """ Makes sure that web service calls are at least 0.2 seconds apart. """ @@ -496,7 +496,7 @@ class _Network: track_number=None, mbid=None, context=None, - ): + ) -> None: """ Used to notify Last.fm that a user has started listening to a track. @@ -583,7 +583,7 @@ class _Network: ) ) - def scrobble_many(self, tracks): + def scrobble_many(self, tracks) -> None: """ Used to scrobble a batch of tracks at once. The parameter tracks is a sequence of dicts per track containing the keyword arguments as if @@ -664,7 +664,7 @@ class LastFMNetwork(_Network): username="", password_hash="", token="", - ): + ) -> None: super().__init__( name="Last.fm", homepage="https://www.last.fm", @@ -728,7 +728,7 @@ class LibreFMNetwork(_Network): def __init__( self, api_key="", api_secret="", session_key="", username="", password_hash="" - ): + ) -> None: super().__init__( name="Libre.fm", @@ -778,7 +778,7 @@ class LibreFMNetwork(_Network): class _ShelfCacheBackend: """Used as a backend for caching cacheable requests.""" - def __init__(self, file_path=None, flag=None): + def __init__(self, file_path=None, flag=None) -> None: if flag is not None: self.shelf = shelve.open(file_path, flag=flag) else: @@ -794,7 +794,7 @@ class _ShelfCacheBackend: def get_xml(self, key): return self.shelf[key] - def set_xml(self, key, xml_string): + def set_xml(self, key, xml_string) -> None: self.cache_keys.add(key) self.shelf[key] = xml_string @@ -808,7 +808,7 @@ class _ShelfCacheBackend: class _Request: """Representing an abstract web service operation.""" - def __init__(self, network, method_name, params=None): + def __init__(self, network, method_name, params=None) -> None: logger.info(method_name) if params is None: @@ -832,7 +832,7 @@ class _Request: self.params["sk"] = self.session_key self.sign_it() - def sign_it(self): + def sign_it(self) -> None: """Sign this request.""" if "api_sig" not in self.params.keys(): @@ -982,7 +982,7 @@ class SessionKeyGenerator: unless you want to. """ - def __init__(self, network): + def __init__(self, network) -> None: self.network = network self.web_auth_tokens = {} @@ -1094,7 +1094,7 @@ class _BaseObject: network = None - def __init__(self, network, ws_prefix): + def __init__(self, network, ws_prefix) -> None: self.network = network self.ws_prefix = ws_prefix @@ -1194,7 +1194,7 @@ class _BaseObject: class _Chartable(_BaseObject): """Common functions for classes with charts.""" - def __init__(self, network, ws_prefix): + def __init__(self, network, ws_prefix) -> None: super().__init__(network=network, ws_prefix=ws_prefix) def get_weekly_chart_dates(self): @@ -1265,10 +1265,10 @@ class _Chartable(_BaseObject): class _Taggable(_BaseObject): """Common functions for classes with tags.""" - def __init__(self, network, ws_prefix): + def __init__(self, network, ws_prefix) -> None: super().__init__(network=network, ws_prefix=ws_prefix) - def add_tags(self, tags): + def add_tags(self, tags) -> None: """Adds one or several tags. * tags: A sequence of tag names or Tag objects. """ @@ -1276,7 +1276,7 @@ class _Taggable(_BaseObject): for tag in tags: self.add_tag(tag) - def add_tag(self, tag): + def add_tag(self, tag) -> None: """Adds one tag. * tag: a tag name or a Tag object. """ @@ -1289,7 +1289,7 @@ class _Taggable(_BaseObject): self._request(self.ws_prefix + ".addTags", False, params) - def remove_tag(self, tag): + def remove_tag(self, tag) -> None: """Remove a user's tag from this object.""" if isinstance(tag, Tag): @@ -1314,7 +1314,7 @@ class _Taggable(_BaseObject): return tags - def remove_tags(self, tags): + def remove_tags(self, tags) -> None: """Removes one or several tags from this object. * tags: a sequence of tag names or Tag objects. """ @@ -1322,12 +1322,12 @@ class _Taggable(_BaseObject): for tag in tags: self.remove_tag(tag) - def clear_tags(self): + def clear_tags(self) -> None: """Clears all the user-set tags.""" self.remove_tags(*(self.get_tags())) - def set_tags(self, tags): + def set_tags(self, tags) -> None: """Sets this object's tags to only those tags. * tags: a sequence of tag names or Tag objects. """ @@ -1390,7 +1390,7 @@ class PyLastError(Exception): class WSError(PyLastError): """Exception related to the Network web service""" - def __init__(self, network, status, details): + def __init__(self, network, status, details) -> None: self.status = status self.details = details self.network = network @@ -1434,7 +1434,7 @@ class WSError(PyLastError): class MalformedResponseError(PyLastError): """Exception conveying a malformed response from the music network.""" - def __init__(self, network, underlying_error): + def __init__(self, network, underlying_error) -> None: self.network = network self.underlying_error = underlying_error @@ -1448,7 +1448,7 @@ class MalformedResponseError(PyLastError): class NetworkError(PyLastError): """Exception conveying a problem in sending a request to Last.fm""" - def __init__(self, network, underlying_error): + def __init__(self, network, underlying_error) -> None: self.network = network self.underlying_error = underlying_error @@ -1465,7 +1465,9 @@ class _Opus(_Taggable): __hash__ = _BaseObject.__hash__ - def __init__(self, artist, title, network, ws_prefix, username=None, info=None): + def __init__( + self, artist, title, network, ws_prefix, username=None, info=None + ) -> None: """ Create an opus instance. # Parameters: @@ -1608,7 +1610,7 @@ class Album(_Opus): __hash__ = _Opus.__hash__ - def __init__(self, artist, title, network, username=None, info=None): + def __init__(self, artist, title, network, username=None, info=None) -> None: super().__init__(artist, title, network, "album", username, info) def get_tracks(self): @@ -1653,7 +1655,7 @@ class Artist(_Taggable): __hash__ = _BaseObject.__hash__ - def __init__(self, name, network, username=None, info=None): + def __init__(self, name, network, username=None, info=None) -> None: """Create an artist object. # Parameters: * name str: The artist's name. @@ -1843,7 +1845,7 @@ class Country(_BaseObject): __hash__ = _BaseObject.__hash__ - def __init__(self, name, network): + def __init__(self, name, network) -> None: super().__init__(network=network, ws_prefix="geo") self.name = name @@ -1918,7 +1920,7 @@ class Library(_BaseObject): __hash__ = _BaseObject.__hash__ - def __init__(self, user, network): + def __init__(self, user, network) -> None: super().__init__(network=network, ws_prefix="library") if isinstance(user, User): @@ -1967,7 +1969,7 @@ class Tag(_Chartable): __hash__ = _BaseObject.__hash__ - def __init__(self, name, network): + def __init__(self, name, network) -> None: super().__init__(network=network, ws_prefix="tag") self.name = name @@ -2054,7 +2056,7 @@ class Track(_Opus): __hash__ = _Opus.__hash__ - def __init__(self, artist, title, network, username=None, info=None): + def __init__(self, artist, title, network, username=None, info=None) -> None: super().__init__(artist, title, network, "track", username, info) def get_correction(self): @@ -2097,12 +2099,12 @@ class Track(_Opus): node = doc.getElementsByTagName("album")[0] return Album(_extract(node, "artist"), _extract(node, "title"), self.network) - def love(self): + def love(self) -> None: """Adds the track to the user's loved tracks.""" self._request(self.ws_prefix + ".love") - def unlove(self): + def unlove(self) -> None: """Remove the track to the user's loved tracks.""" self._request(self.ws_prefix + ".unlove") @@ -2163,7 +2165,7 @@ class User(_Chartable): __hash__ = _BaseObject.__hash__ - def __init__(self, user_name, network): + def __init__(self, user_name, network) -> None: super().__init__(network=network, ws_prefix="user") self.name = user_name @@ -2558,7 +2560,7 @@ class User(_Chartable): class AuthenticatedUser(User): - def __init__(self, network): + def __init__(self, network) -> None: super().__init__(user_name=network.username, network=network) def _get_params(self): @@ -2572,7 +2574,7 @@ class AuthenticatedUser(User): class _Search(_BaseObject): """An abstract class. Use one of its derivatives.""" - def __init__(self, ws_prefix, search_terms, network): + def __init__(self, ws_prefix, search_terms, network) -> None: super().__init__(network, ws_prefix) self._ws_prefix = ws_prefix @@ -2612,7 +2614,7 @@ class _Search(_BaseObject): class AlbumSearch(_Search): """Search for an album by name.""" - def __init__(self, album_name, network): + def __init__(self, album_name, network) -> None: super().__init__( ws_prefix="album", search_terms={"album": album_name}, network=network ) @@ -2639,7 +2641,7 @@ class AlbumSearch(_Search): class ArtistSearch(_Search): """Search for an artist by artist name.""" - def __init__(self, artist_name, network): + def __init__(self, artist_name, network) -> None: super().__init__( ws_prefix="artist", search_terms={"artist": artist_name}, network=network ) @@ -2668,7 +2670,7 @@ class TrackSearch(_Search): down by specifying the artist name, set it to empty string. """ - def __init__(self, artist_name, track_title, network): + def __init__(self, artist_name, track_title, network) -> None: super().__init__( ws_prefix="track", search_terms={"track": track_title, "artist": artist_name}, diff --git a/tests/test_album.py b/tests/test_album.py index 56c469b..ae2c1a0 100755 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -8,7 +8,7 @@ from .test_pylast import TestPyLastWithLastFm class TestPyLastAlbum(TestPyLastWithLastFm): - def test_album_tags_are_topitems(self): + def test_album_tags_are_topitems(self) -> None: # Arrange album = self.network.get_album("Test Artist", "Test Album") @@ -19,14 +19,14 @@ class TestPyLastAlbum(TestPyLastWithLastFm): assert len(tags) > 0 assert isinstance(tags[0], pylast.TopItem) - def test_album_is_hashable(self): + def test_album_is_hashable(self) -> None: # Arrange album = self.network.get_album("Test Artist", "Test Album") # Act/Assert self.helper_is_thing_hashable(album) - def test_album_in_recent_tracks(self): + def test_album_in_recent_tracks(self) -> None: # Arrange lastfm_user = self.network.get_user(self.username) @@ -37,7 +37,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): # Assert assert hasattr(track, "album") - def test_album_wiki_content(self): + def test_album_wiki_content(self) -> None: # Arrange album = pylast.Album("Test Artist", "Test Album", self.network) @@ -48,7 +48,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): assert wiki is not None assert len(wiki) >= 1 - def test_album_wiki_published_date(self): + def test_album_wiki_published_date(self) -> None: # Arrange album = pylast.Album("Test Artist", "Test Album", self.network) @@ -59,7 +59,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): assert wiki is not None assert len(wiki) >= 1 - def test_album_wiki_summary(self): + def test_album_wiki_summary(self) -> None: # Arrange album = pylast.Album("Test Artist", "Test Album", self.network) @@ -70,7 +70,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): assert wiki is not None assert len(wiki) >= 1 - def test_album_eq_none_is_false(self): + def test_album_eq_none_is_false(self) -> None: # Arrange album1 = None album2 = pylast.Album("Test Artist", "Test Album", self.network) @@ -78,7 +78,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): # Act / Assert assert album1 != album2 - def test_album_ne_none_is_true(self): + def test_album_ne_none_is_true(self) -> None: # Arrange album1 = None album2 = pylast.Album("Test Artist", "Test Album", self.network) @@ -86,7 +86,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): # Act / Assert assert album1 != album2 - def test_get_cover_image(self): + def test_get_cover_image(self) -> None: # Arrange album = self.network.get_album("Test Artist", "Test Album") @@ -97,7 +97,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): self.assert_startswith(image, "https://") self.assert_endswith(image, ".gif") - def test_mbid(self): + def test_mbid(self) -> None: # Arrange album = self.network.get_album("Radiohead", "OK Computer") @@ -107,7 +107,7 @@ class TestPyLastAlbum(TestPyLastWithLastFm): # Assert assert mbid == "0b6b4ba0-d36f-47bd-b4ea-6a5b91842d29" - def test_no_mbid(self): + def test_no_mbid(self) -> None: # Arrange album = self.network.get_album("Test Artist", "Test Album") diff --git a/tests/test_artist.py b/tests/test_artist.py index 44bacfd..e72474e 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -10,7 +10,7 @@ from .test_pylast import WRITE_TEST, TestPyLastWithLastFm class TestPyLastArtist(TestPyLastWithLastFm): - def test_repr(self): + def test_repr(self) -> None: # Arrange artist = pylast.Artist("Test Artist", self.network) @@ -20,7 +20,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Assert assert representation.startswith("pylast.Artist('Test Artist',") - def test_artist_is_hashable(self): + def test_artist_is_hashable(self) -> None: # Arrange test_artist = self.network.get_artist("Radiohead") artist = test_artist.get_similar(limit=2)[0].item @@ -29,7 +29,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Act/Assert self.helper_is_thing_hashable(artist) - def test_bio_published_date(self): + def test_bio_published_date(self) -> None: # Arrange artist = pylast.Artist("Test Artist", self.network) @@ -40,7 +40,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert bio is not None assert len(bio) >= 1 - def test_bio_content(self): + def test_bio_content(self) -> None: # Arrange artist = pylast.Artist("Test Artist", self.network) @@ -51,7 +51,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert bio is not None assert len(bio) >= 1 - def test_bio_content_none(self): + def test_bio_content_none(self) -> None: # Arrange # An artist with no biography, with "" in the API XML artist = pylast.Artist("Mr Sizef + Unquote", self.network) @@ -62,7 +62,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Assert assert bio is None - def test_bio_summary(self): + def test_bio_summary(self) -> None: # Arrange artist = pylast.Artist("Test Artist", self.network) @@ -73,7 +73,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert bio is not None assert len(bio) >= 1 - def test_artist_top_tracks(self): + def test_artist_top_tracks(self) -> None: # Arrange # Pick an artist with plenty of plays artist = self.network.get_top_artists(limit=1)[0].item @@ -84,7 +84,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) - def test_artist_top_albums(self): + def test_artist_top_albums(self) -> None: # Arrange # Pick an artist with plenty of plays artist = self.network.get_top_artists(limit=1)[0].item @@ -107,7 +107,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Assert assert len(things) == test_limit - def test_artist_top_albums_limit_default(self): + def test_artist_top_albums_limit_default(self) -> None: # Arrange # Pick an artist with plenty of plays artist = self.network.get_top_artists(limit=1)[0].item @@ -118,7 +118,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Assert assert len(things) == 50 - def test_artist_listener_count(self): + def test_artist_listener_count(self) -> None: # Arrange artist = self.network.get_artist("Test Artist") @@ -130,7 +130,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert count > 0 @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_tag_artist(self): + def test_tag_artist(self) -> None: # Arrange artist = self.network.get_artist("Test Artist") # artist.clear_tags() @@ -145,7 +145,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert found @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_remove_tag_of_type_text(self): + def test_remove_tag_of_type_text(self) -> None: # Arrange tag = "testing" # text artist = self.network.get_artist("Test Artist") @@ -160,7 +160,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert not found @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_remove_tag_of_type_tag(self): + def test_remove_tag_of_type_tag(self) -> None: # Arrange tag = pylast.Tag("testing", self.network) # Tag artist = self.network.get_artist("Test Artist") @@ -175,7 +175,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert not found @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_remove_tags(self): + def test_remove_tags(self) -> None: # Arrange tags = ["removetag1", "removetag2"] artist = self.network.get_artist("Test Artist") @@ -195,7 +195,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert not found2 @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_set_tags(self): + def test_set_tags(self) -> None: # Arrange tags = ["sometag1", "sometag2"] artist = self.network.get_artist("Test Artist 2") @@ -219,7 +219,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert found1 assert found2 - def test_artists(self): + def test_artists(self) -> None: # Arrange artist1 = self.network.get_artist("Radiohead") artist2 = self.network.get_artist("Portishead") @@ -239,7 +239,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): assert url == "https://www.last.fm/music/radiohead" assert mbid == "a74b1b7f-71a5-4011-9441-d0b5e4122711" - def test_artist_eq_none_is_false(self): + def test_artist_eq_none_is_false(self) -> None: # Arrange artist1 = None artist2 = pylast.Artist("Test Artist", self.network) @@ -247,7 +247,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Act / Assert assert artist1 != artist2 - def test_artist_ne_none_is_true(self): + def test_artist_ne_none_is_true(self) -> None: # Arrange artist1 = None artist2 = pylast.Artist("Test Artist", self.network) @@ -255,7 +255,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Act / Assert assert artist1 != artist2 - def test_artist_get_correction(self): + def test_artist_get_correction(self) -> None: # Arrange artist = pylast.Artist("guns and roses", self.network) @@ -265,7 +265,7 @@ class TestPyLastArtist(TestPyLastWithLastFm): # Assert assert corrected_artist_name == "Guns N' Roses" - def test_get_userplaycount(self): + def test_get_userplaycount(self) -> None: # Arrange artist = pylast.Artist("John Lennon", self.network, username=self.username) diff --git a/tests/test_country.py b/tests/test_country.py index 4561d82..6d36ef3 100755 --- a/tests/test_country.py +++ b/tests/test_country.py @@ -8,14 +8,14 @@ from .test_pylast import TestPyLastWithLastFm class TestPyLastCountry(TestPyLastWithLastFm): - def test_country_is_hashable(self): + def test_country_is_hashable(self) -> None: # Arrange country = self.network.get_country("Italy") # Act/Assert self.helper_is_thing_hashable(country) - def test_countries(self): + def test_countries(self) -> None: # Arrange country1 = pylast.Country("Italy", self.network) country2 = pylast.Country("Finland", self.network) diff --git a/tests/test_library.py b/tests/test_library.py index dea876d..e37b771 100755 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -8,7 +8,7 @@ from .test_pylast import TestPyLastWithLastFm class TestPyLastLibrary(TestPyLastWithLastFm): - def test_repr(self): + def test_repr(self) -> None: # Arrange library = pylast.Library(user=self.username, network=self.network) @@ -18,7 +18,7 @@ class TestPyLastLibrary(TestPyLastWithLastFm): # Assert self.assert_startswith(representation, "pylast.Library(") - def test_str(self): + def test_str(self) -> None: # Arrange library = pylast.Library(user=self.username, network=self.network) @@ -28,21 +28,21 @@ class TestPyLastLibrary(TestPyLastWithLastFm): # Assert self.assert_endswith(string, "'s Library") - def test_library_is_hashable(self): + def test_library_is_hashable(self) -> None: # Arrange library = pylast.Library(user=self.username, network=self.network) # Act/Assert self.helper_is_thing_hashable(library) - def test_cacheable_library(self): + def test_cacheable_library(self) -> None: # Arrange library = pylast.Library(self.username, self.network) # Act/Assert self.helper_validate_cacheable(library, "get_artists") - def test_get_user(self): + def test_get_user(self) -> None: # Arrange library = pylast.Library(user=self.username, network=self.network) user_to_get = self.network.get_user(self.username) diff --git a/tests/test_librefm.py b/tests/test_librefm.py index 6b0f3dd..0647976 100755 --- a/tests/test_librefm.py +++ b/tests/test_librefm.py @@ -13,7 +13,7 @@ from .test_pylast import PyLastTestCase, load_secrets class TestPyLastWithLibreFm(PyLastTestCase): """Own class for Libre.fm because we don't need the Last.fm setUp""" - def test_libre_fm(self): + def test_libre_fm(self) -> None: # Arrange secrets = load_secrets() username = secrets["username"] @@ -27,7 +27,7 @@ class TestPyLastWithLibreFm(PyLastTestCase): # Assert assert name == "Radiohead" - def test_repr(self): + def test_repr(self) -> None: # Arrange secrets = load_secrets() username = secrets["username"] diff --git a/tests/test_network.py b/tests/test_network.py index 8937c53..d10cc66 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -13,7 +13,7 @@ from .test_pylast import WRITE_TEST, TestPyLastWithLastFm class TestPyLastNetwork(TestPyLastWithLastFm): @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_scrobble(self): + def test_scrobble(self) -> None: # Arrange artist = "test artist" title = "test title" @@ -31,7 +31,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert str(last_scrobble.track.title).lower() == title @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_update_now_playing(self): + def test_update_now_playing(self) -> None: # Arrange artist = "Test Artist" title = "test title" @@ -55,7 +55,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert len(current_track.info["image"]) assert re.search(r"^http.+$", current_track.info["image"][pylast.SIZE_LARGE]) - def test_enable_rate_limiting(self): + def test_enable_rate_limiting(self) -> None: # Arrange assert not self.network.is_rate_limited() @@ -72,7 +72,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert self.network.is_rate_limited() assert now - then >= 0.2 - def test_disable_rate_limiting(self): + def test_disable_rate_limiting(self) -> None: # Arrange self.network.enable_rate_limit() assert self.network.is_rate_limited() @@ -87,14 +87,14 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert assert not self.network.is_rate_limited() - def test_lastfm_network_name(self): + def test_lastfm_network_name(self) -> None: # Act name = str(self.network) # Assert assert name == "Last.fm Network" - def test_geo_get_top_artists(self): + def test_geo_get_top_artists(self) -> None: # Arrange # Act artists = self.network.get_geo_top_artists(country="United Kingdom", limit=1) @@ -104,7 +104,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert isinstance(artists[0], pylast.TopItem) assert isinstance(artists[0].item, pylast.Artist) - def test_geo_get_top_tracks(self): + def test_geo_get_top_tracks(self) -> None: # Arrange # Act tracks = self.network.get_geo_top_tracks( @@ -116,7 +116,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert isinstance(tracks[0], pylast.TopItem) assert isinstance(tracks[0].item, pylast.Track) - def test_network_get_top_artists_with_limit(self): + def test_network_get_top_artists_with_limit(self) -> None: # Arrange # Act artists = self.network.get_top_artists(limit=1) @@ -124,7 +124,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - def test_network_get_top_tags_with_limit(self): + def test_network_get_top_tags_with_limit(self) -> None: # Arrange # Act tags = self.network.get_top_tags(limit=1) @@ -132,7 +132,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_top_list(tags, pylast.Tag) - def test_network_get_top_tags_with_no_limit(self): + def test_network_get_top_tags_with_no_limit(self) -> None: # Arrange # Act tags = self.network.get_top_tags() @@ -140,7 +140,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert self.helper_at_least_one_thing_in_top_list(tags, pylast.Tag) - def test_network_get_top_tracks_with_limit(self): + def test_network_get_top_tracks_with_limit(self) -> None: # Arrange # Act tracks = self.network.get_top_tracks(limit=1) @@ -148,7 +148,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_top_list(tracks, pylast.Track) - def test_country_top_tracks(self): + def test_country_top_tracks(self) -> None: # Arrange country = self.network.get_country("Croatia") @@ -158,7 +158,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) - def test_country_network_top_tracks(self): + def test_country_network_top_tracks(self) -> None: # Arrange # Act things = self.network.get_geo_top_tracks("Croatia", limit=2) @@ -166,7 +166,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) - def test_tag_top_tracks(self): + def test_tag_top_tracks(self) -> None: # Arrange tag = self.network.get_tag("blues") @@ -176,7 +176,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) - def test_album_data(self): + def test_album_data(self) -> None: # Arrange thing = self.network.get_album("Test Artist", "Test Album") @@ -196,7 +196,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert playcount > 1 assert "https://www.last.fm/music/test%2bartist/test%2balbum" == url - def test_track_data(self): + def test_track_data(self) -> None: # Arrange thing = self.network.get_track("Test Artist", "test title") @@ -217,7 +217,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert playcount > 1 assert "https://www.last.fm/fr/music/test%2bartist/_/test%2btitle" == url - def test_country_top_artists(self): + def test_country_top_artists(self) -> None: # Arrange country = self.network.get_country("Ukraine") @@ -227,7 +227,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - def test_caching(self): + def test_caching(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -242,7 +242,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): self.network.disable_caching() assert not self.network.is_caching_enabled() - def test_album_mbid(self): + def test_album_mbid(self) -> None: # Arrange mbid = "03c91c40-49a6-44a7-90e7-a700edf97a62" @@ -255,7 +255,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert album.title == "Believe" assert album_mbid == mbid - def test_artist_mbid(self): + def test_artist_mbid(self) -> None: # Arrange mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55" @@ -266,7 +266,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert isinstance(artist, pylast.Artist) assert artist.name in ("MusicBrainz Test Artist", "MusicBrainzz Test Artist") - def test_track_mbid(self): + def test_track_mbid(self) -> None: # Arrange mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0" @@ -279,7 +279,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert track.title == "first" assert track_mbid == mbid - def test_init_with_token(self): + def test_init_with_token(self) -> None: # Arrange/Act msg = None try: @@ -294,7 +294,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert assert msg == "Unauthorized Token - This token has not been issued" - def test_proxy(self): + def test_proxy(self) -> None: # Arrange proxy = "http://example.com:1234" @@ -306,7 +306,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): self.network.disable_proxy() assert not self.network.is_proxy_enabled() - def test_album_search(self): + def test_album_search(self) -> None: # Arrange album = "Nevermind" @@ -318,7 +318,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert isinstance(results, list) assert isinstance(results[0], pylast.Album) - def test_album_search_images(self): + def test_album_search_images(self) -> None: # Arrange album = "Nevermind" search = self.network.search_for_album(album) @@ -338,7 +338,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] - def test_artist_search(self): + def test_artist_search(self) -> None: # Arrange artist = "Nirvana" @@ -350,7 +350,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert isinstance(results, list) assert isinstance(results[0], pylast.Artist) - def test_artist_search_images(self): + def test_artist_search_images(self) -> None: # Arrange artist = "Nirvana" search = self.network.search_for_artist(artist) @@ -370,7 +370,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] - def test_track_search(self): + def test_track_search(self) -> None: # Arrange artist = "Nirvana" track = "Smells Like Teen Spirit" @@ -383,7 +383,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): assert isinstance(results, list) assert isinstance(results[0], pylast.Track) - def test_track_search_images(self): + def test_track_search_images(self) -> None: # Arrange artist = "Nirvana" track = "Smells Like Teen Spirit" @@ -404,7 +404,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm): self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] - def test_search_get_total_result_count(self): + def test_search_get_total_result_count(self) -> None: # Arrange artist = "Nirvana" track = "Smells Like Teen Spirit" diff --git a/tests/test_pylast.py b/tests/test_pylast.py index c7cd7b3..08371d6 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -33,10 +33,10 @@ def load_secrets(): # pragma: no cover class PyLastTestCase: - def assert_startswith(self, s, prefix, start=None, end=None): + def assert_startswith(self, s, prefix, start=None, end=None) -> None: assert s.startswith(prefix, start, end) - def assert_endswith(self, s, suffix, start=None, end=None): + def assert_endswith(self, s, suffix, start=None, end=None) -> None: assert s.endswith(suffix, start, end) @@ -54,7 +54,7 @@ class TestPyLastWithLastFm(PyLastTestCase): return int(time.time()) @classmethod - def setup_class(cls): + def setup_class(cls) -> None: if cls.secrets is None: cls.secrets = load_secrets() @@ -71,7 +71,7 @@ class TestPyLastWithLastFm(PyLastTestCase): password_hash=password_hash, ) - def helper_is_thing_hashable(self, thing): + def helper_is_thing_hashable(self, thing) -> None: # Arrange things = set() @@ -82,7 +82,7 @@ class TestPyLastWithLastFm(PyLastTestCase): assert thing is not None assert len(things) == 1 - def helper_validate_results(self, a, b, c): + def helper_validate_results(self, a, b, c) -> None: # Assert assert a is not None assert b is not None @@ -93,7 +93,7 @@ class TestPyLastWithLastFm(PyLastTestCase): assert a == b assert b == c - def helper_validate_cacheable(self, thing, function_name): + def helper_validate_cacheable(self, thing, function_name) -> None: # Arrange # get thing.function_name() func = getattr(thing, function_name, None) @@ -106,27 +106,27 @@ class TestPyLastWithLastFm(PyLastTestCase): # Assert self.helper_validate_results(result1, result2, result3) - def helper_at_least_one_thing_in_top_list(self, things, expected_type): + def helper_at_least_one_thing_in_top_list(self, things, expected_type) -> None: # Assert assert len(things) > 1 assert isinstance(things, list) assert isinstance(things[0], pylast.TopItem) assert isinstance(things[0].item, expected_type) - def helper_only_one_thing_in_top_list(self, things, expected_type): + def helper_only_one_thing_in_top_list(self, things, expected_type) -> None: # Assert assert len(things) == 1 assert isinstance(things, list) assert isinstance(things[0], pylast.TopItem) assert isinstance(things[0].item, expected_type) - def helper_only_one_thing_in_list(self, things, expected_type): + def helper_only_one_thing_in_list(self, things, expected_type) -> None: # Assert assert len(things) == 1 assert isinstance(things, list) assert isinstance(things[0], expected_type) - def helper_two_different_things_in_top_list(self, things, expected_type): + def helper_two_different_things_in_top_list(self, things, expected_type) -> None: # Assert assert len(things) == 2 thing1 = things[0] diff --git a/tests/test_tag.py b/tests/test_tag.py index 65544e0..89080f6 100755 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -8,14 +8,14 @@ from .test_pylast import TestPyLastWithLastFm class TestPyLastTag(TestPyLastWithLastFm): - def test_tag_is_hashable(self): + def test_tag_is_hashable(self) -> None: # Arrange tag = self.network.get_top_tags(limit=1)[0] # Act/Assert self.helper_is_thing_hashable(tag) - def test_tag_top_artists(self): + def test_tag_top_artists(self) -> None: # Arrange tag = self.network.get_tag("blues") @@ -25,7 +25,7 @@ class TestPyLastTag(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - def test_tag_top_albums(self): + def test_tag_top_albums(self) -> None: # Arrange tag = self.network.get_tag("blues") @@ -35,7 +35,7 @@ class TestPyLastTag(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_top_list(albums, pylast.Album) - def test_tags(self): + def test_tags(self) -> None: # Arrange tag1 = self.network.get_tag("blues") tag2 = self.network.get_tag("rock") diff --git a/tests/test_track.py b/tests/test_track.py index 18f1d87..3430e29 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -13,7 +13,7 @@ from .test_pylast import WRITE_TEST, TestPyLastWithLastFm class TestPyLastTrack(TestPyLastWithLastFm): @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_love(self): + def test_love(self) -> None: # Arrange artist = "Test Artist" title = "test title" @@ -29,7 +29,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): assert str(loved[0].track.title).lower() == "test title" @pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") - def test_unlove(self): + def test_unlove(self) -> None: # Arrange artist = pylast.Artist("Test Artist", self.network) title = "test title" @@ -47,7 +47,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): assert str(loved[0].track.artist) != "Test Artist" assert str(loved[0].track.title) != "test title" - def test_user_play_count_in_track_info(self): + def test_user_play_count_in_track_info(self) -> None: # Arrange artist = "Test Artist" title = "test title" @@ -61,7 +61,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert assert count >= 0 - def test_user_loved_in_track_info(self): + def test_user_loved_in_track_info(self) -> None: # Arrange artist = "Test Artist" title = "test title" @@ -77,7 +77,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): assert isinstance(loved, bool) assert not isinstance(loved, str) - def test_track_is_hashable(self): + def test_track_is_hashable(self) -> None: # Arrange artist = self.network.get_artist("Test Artist") track = artist.get_top_tracks(stream=False)[0].item @@ -86,7 +86,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Act/Assert self.helper_is_thing_hashable(track) - def test_track_wiki_content(self): + def test_track_wiki_content(self) -> None: # Arrange track = pylast.Track("Test Artist", "test title", self.network) @@ -97,7 +97,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): assert wiki is not None assert len(wiki) >= 1 - def test_track_wiki_summary(self): + def test_track_wiki_summary(self) -> None: # Arrange track = pylast.Track("Test Artist", "test title", self.network) @@ -108,7 +108,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): assert wiki is not None assert len(wiki) >= 1 - def test_track_get_duration(self): + def test_track_get_duration(self) -> None: # Arrange track = pylast.Track("Cher", "Believe", self.network) @@ -118,7 +118,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert assert duration >= 200000 - def test_track_get_album(self): + def test_track_get_album(self) -> None: # Arrange track = pylast.Track("Nirvana", "Lithium", self.network) @@ -128,7 +128,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert assert str(album) == "Nirvana - Nevermind" - def test_track_get_similar(self): + def test_track_get_similar(self) -> None: # Arrange track = pylast.Track("Cher", "Believe", self.network) @@ -143,7 +143,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): break assert found - def test_track_get_similar_limits(self): + def test_track_get_similar_limits(self) -> None: # Arrange track = pylast.Track("Cher", "Believe", self.network) @@ -153,7 +153,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): assert len(track.get_similar(limit=None)) >= 23 assert len(track.get_similar(limit=0)) >= 23 - def test_tracks_notequal(self): + def test_tracks_notequal(self) -> None: # Arrange track1 = pylast.Track("Test Artist", "test title", self.network) track2 = pylast.Track("Test Artist", "Test Track", self.network) @@ -162,7 +162,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert assert track1 != track2 - def test_track_title_prop_caps(self): + def test_track_title_prop_caps(self) -> None: # Arrange track = pylast.Track("test artist", "test title", self.network) @@ -172,7 +172,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert assert title == "Test Title" - def test_track_listener_count(self): + def test_track_listener_count(self) -> None: # Arrange track = pylast.Track("test artist", "test title", self.network) @@ -182,7 +182,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert assert count > 21 - def test_album_tracks(self): + def test_album_tracks(self) -> None: # Arrange album = pylast.Album("Test Artist", "Test", self.network) @@ -196,7 +196,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): assert len(tracks) == 1 assert url.startswith("https://www.last.fm/music/test") - def test_track_eq_none_is_false(self): + def test_track_eq_none_is_false(self) -> None: # Arrange track1 = None track2 = pylast.Track("Test Artist", "test title", self.network) @@ -204,7 +204,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Act / Assert assert track1 != track2 - def test_track_ne_none_is_true(self): + def test_track_ne_none_is_true(self) -> None: # Arrange track1 = None track2 = pylast.Track("Test Artist", "test title", self.network) @@ -212,7 +212,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Act / Assert assert track1 != track2 - def test_track_get_correction(self): + def test_track_get_correction(self) -> None: # Arrange track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network) @@ -222,7 +222,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert assert corrected_track_name == "Mr. Brownstone" - def test_track_with_no_mbid(self): + def test_track_with_no_mbid(self) -> None: # Arrange track = pylast.Track("Static-X", "Set It Off", self.network) diff --git a/tests/test_user.py b/tests/test_user.py index 5f68262..6e0f3ba 100755 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -16,7 +16,7 @@ from .test_pylast import TestPyLastWithLastFm class TestPyLastUser(TestPyLastWithLastFm): - def test_repr(self): + def test_repr(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -26,7 +26,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert self.assert_startswith(representation, "pylast.User('RJ',") - def test_str(self): + def test_str(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -36,7 +36,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert string == "RJ" - def test_equality(self): + def test_equality(self) -> None: # Arrange user_1a = self.network.get_user("RJ") user_1b = self.network.get_user("RJ") @@ -48,7 +48,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert user_1a != user_2 assert user_1a != not_a_user - def test_get_name(self): + def test_get_name(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -58,7 +58,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert name == "RJ" - def test_get_user_registration(self): + def test_get_user_registration(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -74,7 +74,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Just check date because of timezones assert "2002-11-20 " in registered - def test_get_user_unixtime_registration(self): + def test_get_user_unixtime_registration(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -85,7 +85,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Just check date because of timezones assert unixtime_registered == 1037793040 - def test_get_countryless_user(self): + def test_get_countryless_user(self) -> None: # Arrange # Currently test_user has no country set: lastfm_user = self.network.get_user("test_user") @@ -96,7 +96,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert country is None - def test_user_get_country(self): + def test_user_get_country(self) -> None: # Arrange lastfm_user = self.network.get_user("RJ") @@ -106,7 +106,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert str(country) == "United Kingdom" - def test_user_equals_none(self): + def test_user_equals_none(self) -> None: # Arrange lastfm_user = self.network.get_user(self.username) @@ -116,7 +116,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert not value - def test_user_not_equal_to_none(self): + def test_user_not_equal_to_none(self) -> None: # Arrange lastfm_user = self.network.get_user(self.username) @@ -126,7 +126,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert value - def test_now_playing_user_with_no_scrobbles(self): + def test_now_playing_user_with_no_scrobbles(self) -> None: # Arrange # Currently test-account has no scrobbles: user = self.network.get_user("test-account") @@ -137,7 +137,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert current_track is None - def test_love_limits(self): + def test_love_limits(self) -> None: # Arrange # Currently test-account has at least 23 loved tracks: user = self.network.get_user("test-user") @@ -148,7 +148,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert len(user.get_loved_tracks(limit=None)) >= 23 assert len(user.get_loved_tracks(limit=0)) >= 23 - def test_user_is_hashable(self): + def test_user_is_hashable(self) -> None: # Arrange user = self.network.get_user(self.username) @@ -169,7 +169,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # # Assert # self.assertGreaterEqual(len(tracks), 0) - def test_pickle(self): + def test_pickle(self) -> None: # Arrange import pickle @@ -187,7 +187,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert lastfm_user == loaded_user @pytest.mark.xfail - def test_cacheable_user(self): + def test_cacheable_user(self) -> None: # Arrange lastfm_user = self.network.get_authenticated_user() @@ -201,7 +201,7 @@ class TestPyLastUser(TestPyLastWithLastFm): lastfm_user, "get_recent_tracks" ) - def test_user_get_top_tags_with_limit(self): + def test_user_get_top_tags_with_limit(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -211,7 +211,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_top_list(tags, pylast.Tag) - def test_user_top_tracks(self): + def test_user_top_tracks(self) -> None: # Arrange lastfm_user = self.network.get_user("RJ") @@ -221,14 +221,14 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert self.helper_two_different_things_in_top_list(things, pylast.Track) - def helper_assert_chart(self, chart, expected_type): + def helper_assert_chart(self, chart, expected_type) -> None: # Assert assert chart is not None assert len(chart) > 0 assert isinstance(chart[0], pylast.TopItem) assert isinstance(chart[0].item, expected_type) - def helper_get_assert_charts(self, thing, date): + def helper_get_assert_charts(self, thing, date) -> None: # Arrange album_chart, track_chart = None, None (from_date, to_date) = date @@ -245,14 +245,14 @@ class TestPyLastUser(TestPyLastWithLastFm): self.helper_assert_chart(album_chart, pylast.Album) self.helper_assert_chart(track_chart, pylast.Track) - def helper_dates_valid(self, dates): + def helper_dates_valid(self, dates) -> None: # Assert assert len(dates) >= 1 assert isinstance(dates[0], tuple) (start, end) = dates[0] assert start < end - def test_user_charts(self): + def test_user_charts(self) -> None: # Arrange lastfm_user = self.network.get_user("RJ") dates = lastfm_user.get_weekly_chart_dates() @@ -261,7 +261,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Act/Assert self.helper_get_assert_charts(lastfm_user, dates[0]) - def test_user_top_artists(self): + def test_user_top_artists(self) -> None: # Arrange lastfm_user = self.network.get_user(self.username) @@ -271,7 +271,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_top_list(artists, pylast.Artist) - def test_user_top_albums(self): + def test_user_top_albums(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -285,7 +285,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert len(top_album.info["image"]) assert re.search(r"^http.+$", top_album.info["image"][pylast.SIZE_LARGE]) - def test_user_tagged_artists(self): + def test_user_tagged_artists(self) -> None: # Arrange lastfm_user = self.network.get_user(self.username) tags = ["artisttagola"] @@ -298,7 +298,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_list(artists, pylast.Artist) - def test_user_tagged_albums(self): + def test_user_tagged_albums(self) -> None: # Arrange lastfm_user = self.network.get_user(self.username) tags = ["albumtagola"] @@ -311,7 +311,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_list(albums, pylast.Album) - def test_user_tagged_tracks(self): + def test_user_tagged_tracks(self) -> None: # Arrange lastfm_user = self.network.get_user(self.username) tags = ["tracktagola"] @@ -324,7 +324,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert self.helper_only_one_thing_in_list(tracks, pylast.Track) - def test_user_subscriber(self): + def test_user_subscriber(self) -> None: # Arrange subscriber = self.network.get_user("RJ") non_subscriber = self.network.get_user("Test User") @@ -337,7 +337,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert subscriber_is_subscriber assert not non_subscriber_is_subscriber - def test_user_get_image(self): + def test_user_get_image(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -347,7 +347,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert self.assert_startswith(url, "https://") - def test_user_get_library(self): + def test_user_get_library(self) -> None: # Arrange user = self.network.get_user(self.username) @@ -357,7 +357,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert isinstance(library, pylast.Library) - def test_get_recent_tracks_from_to(self): + def test_get_recent_tracks_from_to(self) -> None: # Arrange lastfm_user = self.network.get_user("RJ") start = dt.datetime(2011, 7, 21, 15, 10) @@ -374,7 +374,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert str(tracks[0].track.artist) == "Johnny Cash" assert str(tracks[0].track.title) == "Ring of Fire" - def test_get_recent_tracks_limit_none(self): + def test_get_recent_tracks_limit_none(self) -> None: # Arrange lastfm_user = self.network.get_user("bbc6music") start = dt.datetime(2020, 2, 15, 15, 00) @@ -393,7 +393,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert str(tracks[0].track.artist) == "Seun Kuti & Egypt 80" assert str(tracks[0].track.title) == "Struggles Sounds" - def test_get_recent_tracks_is_streamable(self): + def test_get_recent_tracks_is_streamable(self) -> None: # Arrange lastfm_user = self.network.get_user("bbc6music") start = dt.datetime(2020, 2, 15, 15, 00) @@ -410,7 +410,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert inspect.isgenerator(tracks) - def test_get_playcount(self): + def test_get_playcount(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -420,7 +420,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert playcount >= 128387 - def test_get_image(self): + def test_get_image(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -431,7 +431,7 @@ class TestPyLastUser(TestPyLastWithLastFm): self.assert_startswith(image, "https://") self.assert_endswith(image, ".png") - def test_get_url(self): + def test_get_url(self) -> None: # Arrange user = self.network.get_user("RJ") @@ -441,7 +441,7 @@ class TestPyLastUser(TestPyLastWithLastFm): # Assert assert url == "https://www.last.fm/user/rj" - def test_get_weekly_artist_charts(self): + def test_get_weekly_artist_charts(self) -> None: # Arrange user = self.network.get_user("bbc6music") @@ -453,7 +453,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert artist is not None assert isinstance(artist.network, pylast.LastFMNetwork) - def test_get_weekly_track_charts(self): + def test_get_weekly_track_charts(self) -> None: # Arrange user = self.network.get_user("bbc6music") @@ -465,7 +465,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert track is not None assert isinstance(track.network, pylast.LastFMNetwork) - def test_user_get_track_scrobbles(self): + def test_user_get_track_scrobbles(self) -> None: # Arrange artist = "France Gall" title = "Laisse Tomber Les Filles" @@ -479,7 +479,7 @@ class TestPyLastUser(TestPyLastWithLastFm): assert str(scrobbles[0].track.artist) == "France Gall" assert scrobbles[0].track.title == "Laisse Tomber Les Filles" - def test_cacheable_user_get_track_scrobbles(self): + def test_cacheable_user_get_track_scrobbles(self) -> None: # Arrange artist = "France Gall" title = "Laisse Tomber Les Filles" diff --git a/tests/unicode_test.py b/tests/unicode_test.py index 350256c..bc93dfa 100644 --- a/tests/unicode_test.py +++ b/tests/unicode_test.py @@ -18,13 +18,13 @@ def mock_network(): "fdasfdsafsaf not unicode", ], ) -def test_get_cache_key(artist): +def test_get_cache_key(artist) -> None: request = pylast._Request(mock_network(), "some_method", params={"artist": artist}) request._get_cache_key() @pytest.mark.parametrize("obj", [pylast.Artist("B\xe9l", mock_network())]) -def test_cast_and_hash(obj): +def test_cast_and_hash(obj) -> None: assert type(str(obj)) is str assert isinstance(hash(obj), int) From eb4af40d641c965ab595def7eb69f245675e78f5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:45:23 +0300 Subject: [PATCH 054/138] autotyping: --scalar-return: add a return annotation to functions that only return literal bool, str, bytes, int, or float objects --- src/pylast/__init__.py | 26 +++++++++++++------------- tests/test_pylast.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 231576f..f33ac54 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -215,7 +215,7 @@ class _Network: sk_gen = SessionKeyGenerator(self) self.session_key = sk_gen.get_session_key(self.username, self.password_hash) - def __str__(self): + def __str__(self) -> str: return f"{self.name} Network" def get_artist(self, artist_name): @@ -275,7 +275,7 @@ class _Network: if domain_language in self.domain_names: return self.domain_names[domain_language] - def _get_url(self, domain, url_type): + def _get_url(self, domain, url_type) -> str: return f"https://{self._get_language_domain(domain)}/{self.urls[url_type]}" def _get_ws_auth(self): @@ -699,7 +699,7 @@ class LastFMNetwork(_Network): }, ) - def __repr__(self): + def __repr__(self) -> str: return ( "pylast.LastFMNetwork(" f"'{self.api_key}', " @@ -763,7 +763,7 @@ class LibreFMNetwork(_Network): }, ) - def __repr__(self): + def __repr__(self) -> str: return ( "pylast.LibreFMNetwork(" f"'{self.api_key}', " @@ -1438,7 +1438,7 @@ class MalformedResponseError(PyLastError): self.network = network self.underlying_error = underlying_error - def __str__(self): + def __str__(self) -> str: return ( f"Malformed response from {self.network.name}. " f"Underlying error: {self.underlying_error}" @@ -1452,7 +1452,7 @@ class NetworkError(PyLastError): self.network = network self.underlying_error = underlying_error - def __str__(self): + def __str__(self) -> str: return f"NetworkError: {self.underlying_error}" @@ -1492,14 +1492,14 @@ class _Opus(_Taggable): ) # Default to current user self.info = info - def __repr__(self): + def __repr__(self) -> str: return ( f"pylast.{self.ws_prefix.title()}" f"({repr(self.artist.name)}, {repr(self.title)}, {repr(self.network)})" ) @_string_output - def __str__(self): + def __str__(self) -> str: return f"{self.get_artist().get_name()} - {self.get_title()}" def __eq__(self, other): @@ -1670,7 +1670,7 @@ class Artist(_Taggable): self.username = username self.info = info - def __repr__(self): + def __repr__(self) -> str: return f"pylast.Artist({repr(self.get_name())}, {repr(self.network)})" def __unicode__(self): @@ -1850,7 +1850,7 @@ class Country(_BaseObject): self.name = name - def __repr__(self): + def __repr__(self) -> str: return f"pylast.Country({repr(self.name)}, {repr(self.network)})" @_string_output @@ -1928,7 +1928,7 @@ class Library(_BaseObject): else: self.user = User(user, self.network) - def __repr__(self): + def __repr__(self) -> str: return f"pylast.Library({repr(self.user)}, {repr(self.network)})" @_string_output @@ -1974,7 +1974,7 @@ class Tag(_Chartable): self.name = name - def __repr__(self): + def __repr__(self) -> str: return f"pylast.Tag({repr(self.name)}, {repr(self.network)})" @_string_output @@ -2170,7 +2170,7 @@ class User(_Chartable): self.name = user_name - def __repr__(self): + def __repr__(self) -> str: return f"pylast.User({repr(self.name)}, {repr(self.network)})" @_string_output diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 08371d6..7125413 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -40,7 +40,7 @@ class PyLastTestCase: assert s.endswith(suffix, start, end) -def _no_xfail_rerun_filter(err, name, test, plugin): +def _no_xfail_rerun_filter(err, name, test, plugin) -> bool: for _ in test.iter_markers(name="xfail"): return False From 5ab3e53a44c7962d3a5df805c1b7f84c65553a9a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:46:14 +0300 Subject: [PATCH 055/138] autotyping: --bool-param: add a : bool annotation to any function parameter with a default of True or False --- src/pylast/__init__.py | 81 +++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index f33ac54..70280b6 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -297,7 +297,7 @@ class _Network: self.last_call_time = now - def get_top_artists(self, limit=None, cacheable=True): + def get_top_artists(self, limit=None, cacheable: bool = True): """Returns the most played artists as a sequence of TopItem objects.""" params = {} @@ -308,7 +308,7 @@ class _Network: return _extract_top_artists(doc, self) - def get_top_tracks(self, limit=None, cacheable=True): + def get_top_tracks(self, limit=None, cacheable: bool = True): """Returns the most played tracks as a sequence of TopItem objects.""" params = {} @@ -327,7 +327,7 @@ class _Network: return seq - def get_top_tags(self, limit=None, cacheable=True): + def get_top_tags(self, limit=None, cacheable: bool = True): """Returns the most used tags as a sequence of TopItem objects.""" # Last.fm has no "limit" parameter for tag.getTopTags @@ -344,7 +344,7 @@ class _Network: return seq - def get_geo_top_artists(self, country, limit=None, cacheable=True): + def get_geo_top_artists(self, country, limit=None, cacheable: bool = True): """Get the most popular artists on Last.fm by country. Parameters: country (Required) : A country name, as defined by the ISO 3166-1 @@ -361,7 +361,9 @@ class _Network: return _extract_top_artists(doc, self) - def get_geo_top_tracks(self, country, location=None, limit=None, cacheable=True): + def get_geo_top_tracks( + self, country, location=None, limit=None, cacheable: bool = True + ): """Get the most popular tracks on Last.fm last week by country. Parameters: country (Required) : A country name, as defined by the ISO 3166-1 @@ -1098,7 +1100,7 @@ class _BaseObject: self.network = network self.ws_prefix = ws_prefix - def _request(self, method_name, cacheable=False, params=None): + def _request(self, method_name, cacheable: bool = False, params=None): if not params: params = self._get_params() @@ -1129,7 +1131,12 @@ class _BaseObject: return first_child.wholeText.strip() def _get_things( - self, method, thing_type, params=None, cacheable=True, stream=False + self, + method, + thing_type, + params=None, + cacheable: bool = True, + stream: bool = False, ): """Returns a list of the most played thing_types by this thing.""" @@ -1540,7 +1547,7 @@ class _Opus(_Taggable): ) return self.info["image"][size] - def get_title(self, properly_capitalized=False): + def get_title(self, properly_capitalized: bool = False): """Returns the artist or track title.""" if properly_capitalized: self.title = _extract( @@ -1549,7 +1556,7 @@ class _Opus(_Taggable): return self.title - def get_name(self, properly_capitalized=False): + def get_name(self, properly_capitalized: bool = False): """Returns the album or track title (alias to get_title()).""" return self.get_title(properly_capitalized) @@ -1692,7 +1699,7 @@ class Artist(_Taggable): def _get_params(self): return {self.ws_prefix: self.get_name()} - def get_name(self, properly_capitalized=False): + def get_name(self, properly_capitalized: bool = False): """Returns the name of the artist. If properly_capitalized was asserted then the name would be downloaded overwriting the given one.""" @@ -1799,7 +1806,7 @@ class Artist(_Taggable): return artists - def get_top_albums(self, limit=None, cacheable=True, stream=False): + def get_top_albums(self, limit=None, cacheable: bool = True, stream: bool = False): """Returns a list of the top albums.""" params = self._get_params() if limit: @@ -1807,7 +1814,7 @@ class Artist(_Taggable): return self._get_things("getTopAlbums", Album, params, cacheable, stream=stream) - def get_top_tracks(self, limit=None, cacheable=True, stream=False): + def get_top_tracks(self, limit=None, cacheable: bool = True, stream: bool = False): """Returns a list of the most played Tracks by this artist.""" params = self._get_params() if limit: @@ -1871,7 +1878,7 @@ class Country(_BaseObject): return self.name - def get_top_artists(self, limit=None, cacheable=True): + def get_top_artists(self, limit=None, cacheable: bool = True): """Returns a sequence of the most played artists.""" params = self._get_params() if limit: @@ -1881,7 +1888,7 @@ class Country(_BaseObject): return _extract_top_artists(doc, self) - def get_top_tracks(self, limit=None, cacheable=True, stream=False): + def get_top_tracks(self, limit=None, cacheable: bool = True, stream: bool = False): """Returns a sequence of the most played tracks""" params = self._get_params() if limit: @@ -1942,7 +1949,7 @@ class Library(_BaseObject): """Returns the user who owns this library.""" return self.user - def get_artists(self, limit=50, cacheable=True, stream=False): + def get_artists(self, limit=50, cacheable: bool = True, stream: bool = False): """ Returns a sequence of Album objects if limit==None it will return all (may take a while) @@ -1990,7 +1997,7 @@ class Tag(_Chartable): def _get_params(self): return {self.ws_prefix: self.get_name()} - def get_name(self, properly_capitalized=False): + def get_name(self, properly_capitalized: bool = False): """Returns the name of the tag.""" if properly_capitalized: @@ -2000,7 +2007,7 @@ class Tag(_Chartable): return self.name - def get_top_albums(self, limit=None, cacheable=True): + def get_top_albums(self, limit=None, cacheable: bool = True): """Returns a list of the top albums.""" params = self._get_params() if limit: @@ -2010,7 +2017,7 @@ class Tag(_Chartable): return _extract_top_albums(doc, self.network) - def get_top_tracks(self, limit=None, cacheable=True, stream=False): + def get_top_tracks(self, limit=None, cacheable: bool = True, stream: bool = False): """Returns a list of the most played Tracks for this tag.""" params = self._get_params() if limit: @@ -2018,7 +2025,7 @@ class Tag(_Chartable): return self._get_things("getTopTracks", Track, params, cacheable, stream=stream) - def get_top_artists(self, limit=None, cacheable=True): + def get_top_artists(self, limit=None, cacheable: bool = True): """Returns a sequence of the most played artists.""" params = self._get_params() @@ -2199,7 +2206,7 @@ class User(_Chartable): Track(track_artist, title, self.network), album, date, timestamp ) - def get_name(self, properly_capitalized=False): + def get_name(self, properly_capitalized: bool = False): """Returns the user name.""" if properly_capitalized: @@ -2209,7 +2216,7 @@ class User(_Chartable): return self.name - def get_friends(self, limit=50, cacheable=False, stream=False): + def get_friends(self, limit=50, cacheable: bool = False, stream: bool = False): """Returns a list of the user's friends.""" def _get_friends(): @@ -2220,7 +2227,7 @@ class User(_Chartable): return _get_friends() if stream else list(_get_friends()) - def get_loved_tracks(self, limit=50, cacheable=True, stream=False): + def get_loved_tracks(self, limit=50, cacheable: bool = True, stream: bool = False): """ Returns this user's loved track as a sequence of LovedTrack objects in reverse order of their timestamp, all the way back to the first track. @@ -2286,11 +2293,11 @@ class User(_Chartable): def get_recent_tracks( self, limit=10, - cacheable=True, + cacheable: bool = True, time_from=None, time_to=None, - stream=False, - now_playing=False, + stream: bool = False, + now_playing: bool = False, ): """ Returns this user's played track as a sequence of PlayedTrack objects @@ -2380,7 +2387,7 @@ class User(_Chartable): return int(doc.getElementsByTagName("registered")[0].getAttribute("unixtime")) - def get_tagged_albums(self, tag, limit=None, cacheable=True): + def get_tagged_albums(self, tag, limit=None, cacheable: bool = True): """Returns the albums tagged by a user.""" params = self._get_params() @@ -2402,7 +2409,7 @@ class User(_Chartable): doc = self._request(self.ws_prefix + ".getpersonaltags", True, params) return _extract_artists(doc, self.network) - def get_tagged_tracks(self, tag, limit=None, cacheable=True): + def get_tagged_tracks(self, tag, limit=None, cacheable: bool = True): """Returns the tracks tagged by a user.""" params = self._get_params() @@ -2413,7 +2420,7 @@ class User(_Chartable): doc = self._request(self.ws_prefix + ".getpersonaltags", cacheable, params) return _extract_tracks(doc, self.network) - def get_top_albums(self, period=PERIOD_OVERALL, limit=None, cacheable=True): + def get_top_albums(self, period=PERIOD_OVERALL, limit=None, cacheable: bool = True): """Returns the top albums played by a user. * period: The period of time. Possible values: o PERIOD_OVERALL @@ -2453,7 +2460,7 @@ class User(_Chartable): return _extract_top_artists(doc, self.network) - def get_top_tags(self, limit=None, cacheable=True): + def get_top_tags(self, limit=None, cacheable: bool = True): """ Returns a sequence of the top tags used by this user with their counts as TopItem objects. @@ -2478,7 +2485,11 @@ class User(_Chartable): return seq def get_top_tracks( - self, period=PERIOD_OVERALL, limit=None, cacheable=True, stream=False + self, + period=PERIOD_OVERALL, + limit=None, + cacheable: bool = True, + stream: bool = False, ): """Returns the top tracks played by a user. * period: The period of time. Possible values: @@ -2496,7 +2507,9 @@ class User(_Chartable): return self._get_things("getTopTracks", Track, params, cacheable, stream=stream) - def get_track_scrobbles(self, artist, track, cacheable=False, stream=False): + def get_track_scrobbles( + self, artist, track, cacheable: bool = False, stream: bool = False + ): """ Get a list of this user's scrobbles of this artist's track, including scrobble time. @@ -2566,7 +2579,7 @@ class AuthenticatedUser(User): def _get_params(self): return {"user": self.get_name()} - def get_name(self, properly_capitalized=False): + def get_name(self, properly_capitalized: bool = False): """Returns the name of the authenticated user.""" return super().get_name(properly_capitalized=properly_capitalized) @@ -2722,7 +2735,9 @@ def cleanup_nodes(doc): return doc -def _collect_nodes(limit, sender, method_name, cacheable, params=None, stream=False): +def _collect_nodes( + limit, sender, method_name, cacheable, params=None, stream: bool = False +): """ Returns a sequence of dom.Node objects about as close to limit as possible """ From 54ea354a7a510d44c26b04123e1b90d3a65ab91e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:47:10 +0300 Subject: [PATCH 056/138] autotyping: --int-param, --float-param, --str-param, --bytes-param: add an annotation to any parameter for which the default is a literal int, float, str, or bytes object --- src/pylast/__init__.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 70280b6..2a48fab 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -660,12 +660,12 @@ class LastFMNetwork(_Network): def __init__( self, - api_key="", - api_secret="", - session_key="", - username="", - password_hash="", - token="", + api_key: str = "", + api_secret: str = "", + session_key: str = "", + username: str = "", + password_hash: str = "", + token: str = "", ) -> None: super().__init__( name="Last.fm", @@ -729,7 +729,12 @@ class LibreFMNetwork(_Network): """ def __init__( - self, api_key="", api_secret="", session_key="", username="", password_hash="" + self, + api_key: str = "", + api_secret: str = "", + session_key: str = "", + username: str = "", + password_hash: str = "", ) -> None: super().__init__( @@ -1024,7 +1029,7 @@ class SessionKeyGenerator: return url - def get_web_auth_session_key_username(self, url, token=""): + def get_web_auth_session_key_username(self, url, token: str = ""): """ Retrieves the session key/username of a web authorization process by its URL. """ @@ -1044,7 +1049,7 @@ class SessionKeyGenerator: username = doc.getElementsByTagName("name")[0].firstChild.data return session_key, username - def get_web_auth_session_key(self, url, token=""): + def get_web_auth_session_key(self, url, token: str = ""): """ Retrieves the session key of a web authorization process by its URL. """ @@ -1949,7 +1954,9 @@ class Library(_BaseObject): """Returns the user who owns this library.""" return self.user - def get_artists(self, limit=50, cacheable: bool = True, stream: bool = False): + def get_artists( + self, limit: int = 50, cacheable: bool = True, stream: bool = False + ): """ Returns a sequence of Album objects if limit==None it will return all (may take a while) @@ -2216,7 +2223,9 @@ class User(_Chartable): return self.name - def get_friends(self, limit=50, cacheable: bool = False, stream: bool = False): + def get_friends( + self, limit: int = 50, cacheable: bool = False, stream: bool = False + ): """Returns a list of the user's friends.""" def _get_friends(): @@ -2227,7 +2236,9 @@ class User(_Chartable): return _get_friends() if stream else list(_get_friends()) - def get_loved_tracks(self, limit=50, cacheable: bool = True, stream: bool = False): + def get_loved_tracks( + self, limit: int = 50, cacheable: bool = True, stream: bool = False + ): """ Returns this user's loved track as a sequence of LovedTrack objects in reverse order of their timestamp, all the way back to the first track. @@ -2292,7 +2303,7 @@ class User(_Chartable): def get_recent_tracks( self, - limit=10, + limit: int = 10, cacheable: bool = True, time_from=None, time_to=None, @@ -2792,7 +2803,7 @@ def _collect_nodes( return _stream_collect_nodes() if stream else list(_stream_collect_nodes()) -def _extract(node, name, index=0): +def _extract(node, name, index: int = 0): """Extracts a value from the xml string""" nodes = node.getElementsByTagName(name) From 7b9c73acb70386e086525b4de78b1fa055451f86 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:47:58 +0300 Subject: [PATCH 057/138] autotyping: --annotate-magics: add type annotation to certain magic methods --- src/pylast/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 2a48fab..3ed1604 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -792,7 +792,7 @@ class _ShelfCacheBackend: self.shelf = shelve.open(file_path) self.cache_keys = set(self.shelf.keys()) - def __contains__(self, key): + def __contains__(self, key) -> bool: return key in self.cache_keys def __iter__(self): @@ -1408,7 +1408,7 @@ class WSError(PyLastError): self.network = network @_string_output - def __str__(self): + def __str__(self) -> str: return self.details def get_id(self): @@ -1689,7 +1689,7 @@ class Artist(_Taggable): return str(self.get_name()) @_string_output - def __str__(self): + def __str__(self) -> str: return self.__unicode__() def __eq__(self, other): @@ -1866,7 +1866,7 @@ class Country(_BaseObject): return f"pylast.Country({repr(self.name)}, {repr(self.network)})" @_string_output - def __str__(self): + def __str__(self) -> str: return self.get_name() def __eq__(self, other): @@ -1944,7 +1944,7 @@ class Library(_BaseObject): return f"pylast.Library({repr(self.user)}, {repr(self.network)})" @_string_output - def __str__(self): + def __str__(self) -> str: return repr(self.get_user()) + "'s Library" def _get_params(self): @@ -1992,7 +1992,7 @@ class Tag(_Chartable): return f"pylast.Tag({repr(self.name)}, {repr(self.network)})" @_string_output - def __str__(self): + def __str__(self) -> str: return self.get_name() def __eq__(self, other): @@ -2188,7 +2188,7 @@ class User(_Chartable): return f"pylast.User({repr(self.name)}, {repr(self.network)})" @_string_output - def __str__(self): + def __str__(self) -> str: return self.get_name() def __eq__(self, other): From 14e091c8706881acaf88f0a19a86e84ffaee1f9b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:49:01 +0300 Subject: [PATCH 058/138] autotyping: --annotate-imprecise-magics: add imprecise type annotations for some additional magic methods --- src/pylast/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 3ed1604..162d0f1 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -31,6 +31,7 @@ import ssl import tempfile import time import xml.dom +from typing import Iterator from urllib.parse import quote_plus from xml.dom import Node, minidom @@ -795,7 +796,7 @@ class _ShelfCacheBackend: def __contains__(self, key) -> bool: return key in self.cache_keys - def __iter__(self): + def __iter__(self) -> Iterator: return iter(self.shelf.keys()) def get_xml(self, key): From ac991cbd2c4fc623bc0cc19894eb49ebeef47343 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:58:44 +0300 Subject: [PATCH 059/138] Types and typos --- src/pylast/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 162d0f1..4b27806 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -42,7 +42,7 @@ try: import importlib.metadata as importlib_metadata except ImportError: # Python 3.7 and lower - import importlib_metadata + import importlib_metadata # type: ignore __author__ = "Amr Hassan, hugovk, Mice Pápai" __copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2021 hugovk, 2017 Mice Pápai" @@ -197,7 +197,7 @@ class _Network: self.cache_backend = None self.proxy = None - self.last_call_time = 0 + self.last_call_time: float = 0 self.limit_rate = False # Load session_key and username from authentication token if provided @@ -335,7 +335,7 @@ class _Network: # so we need to get all (250) and then limit locally doc = _Request(self, "tag.getTopTags").execute(cacheable) - seq = [] + seq: list[TopItem] = [] for node in doc.getElementsByTagName("tag"): if limit and len(seq) >= limit: break @@ -449,13 +449,13 @@ class _Network: return AlbumSearch(album_name, self) def search_for_artist(self, artist_name): - """Searches of an artist by its name. Returns an ArtistSearch object. + """Searches for an artist by its name. Returns an ArtistSearch object. Use get_next_page() to retrieve sequences of results.""" return ArtistSearch(artist_name, self) def search_for_track(self, artist_name, track_name): - """Searches of a track by its name and its artist. Set artist to an + """Searches for a track by its name and its artist. Set artist to an empty string if not available. Returns a TrackSearch object. Use get_next_page() to retrieve sequences of results.""" From 4f37ba41bdcc598f6df27d2555c2dde6a2105dd1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 18:11:42 +0300 Subject: [PATCH 060/138] Initialise float as 0.0 And skip Iterator type for now to avoid its complex subscripting --- src/pylast/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 4b27806..e152c31 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -31,7 +31,6 @@ import ssl import tempfile import time import xml.dom -from typing import Iterator from urllib.parse import quote_plus from xml.dom import Node, minidom @@ -197,7 +196,7 @@ class _Network: self.cache_backend = None self.proxy = None - self.last_call_time: float = 0 + self.last_call_time: float = 0.0 self.limit_rate = False # Load session_key and username from authentication token if provided @@ -796,7 +795,7 @@ class _ShelfCacheBackend: def __contains__(self, key) -> bool: return key in self.cache_keys - def __iter__(self) -> Iterator: + def __iter__(self): return iter(self.shelf.keys()) def get_xml(self, key): From c1a8a9455f0efd4a64a64ff7766417e22f1281f5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 17:19:23 +0000 Subject: [PATCH 061/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.31.0 → v2.31.1](https://github.com/asottile/pyupgrade/compare/v2.31.0...v2.31.1) - [github.com/psf/black: 21.12b0 → 22.3.0](https://github.com/psf/black/compare/21.12b0...22.3.0) - [github.com/asottile/blacken-docs: v1.12.0 → v1.12.1](https://github.com/asottile/blacken-docs/compare/v1.12.0...v1.12.1) - [github.com/asottile/setup-cfg-fmt: v1.20.0 → v1.20.1](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.0...v1.20.1) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23bc55c..9fd1e6d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,18 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.3.0 hooks: - id: black args: [--target-version=py37] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.0 + rev: v1.12.1 hooks: - id: blacken-docs args: [--target-version=py37] @@ -41,7 +41,7 @@ repos: - id: check-yaml - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.0 + rev: v1.20.1 hooks: - id: setup-cfg-fmt From 2478980ca50c0576e8866271f9cef194672c9672 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 3 Apr 2022 12:20:48 +0300 Subject: [PATCH 062/138] For some reason the earlier track is returning duration=0 --- tests/test_track.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_track.py b/tests/test_track.py index 00f4eca..18f1d87 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -110,7 +110,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): def test_track_get_duration(self): # Arrange - track = pylast.Track("Nirvana", "Lithium", self.network) + track = pylast.Track("Cher", "Believe", self.network) # Act duration = track.get_duration() From 5b0c879fa0e1510a7b3287f6ae0fbf14424769a2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 May 2022 12:02:25 +0300 Subject: [PATCH 063/138] Update config --- .github/workflows/deploy.yml | 4 ++-- .github/workflows/lint.yml | 2 +- .github/workflows/require-pr-label.yml | 18 ++++++++++++++++++ .github/workflows/test.yml | 9 +++------ setup.py | 2 +- 5 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/require-pr-label.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5bd3973..5002205 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,7 +12,7 @@ on: jobs: deploy: if: github.repository_owner == 'pylast' - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -24,7 +24,7 @@ jobs: with: python-version: "3.10" cache: pip - cache-dependency-path: "setup.py" + cache-dependency-path: setup.cfg - name: Install dependencies run: | diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d9014a8..b095963 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,7 +4,7 @@ on: [push, pull_request, workflow_dispatch] jobs: lint: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/require-pr-label.yml b/.github/workflows/require-pr-label.yml new file mode 100644 index 0000000..a2c74d5 --- /dev/null +++ b/.github/workflows/require-pr-label.yml @@ -0,0 +1,18 @@ +name: Require PR label + +on: + pull_request: + types: [opened, reopened, labeled, unlabeled, synchronize] + +jobs: + label: + runs-on: ubuntu-latest + + steps: + - uses: mheap/github-action-required-labels@v1 + with: + mode: minimum + count: 1 + labels: + "changelog: Added, changelog: Changed, changelog: Deprecated, changelog: + Fixed, changelog: Removed, changelog: Security, changelog: skip" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9dc716..199d2e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,10 +12,7 @@ jobs: fail-fast: false matrix: python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10"] - os: [ubuntu-20.04] - include: - # Include new variables for Codecov - - { codecov-flag: GHA_Ubuntu2004, os: ubuntu-20.04 } + os: [ubuntu-latest] steps: - uses: actions/checkout@v2 @@ -25,7 +22,7 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: pip - cache-dependency-path: "setup.py" + cache-dependency-path: setup.cfg - name: Install dependencies run: | @@ -45,5 +42,5 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v2 with: - flags: ${{ matrix.codecov-flag }} + flags: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/setup.py b/setup.py index 668794a..630d39e 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup -def local_scheme(version) -> str: +def local_scheme(version: str) -> str: """Skip the local version (eg. +xyz of 0.6.1.dev4+gdf99fe2) to be able to upload to Test PyPI""" return "" From fa94ed02635817bb62f246ac6694f5282cf46710 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 May 2022 12:04:58 +0300 Subject: [PATCH 064/138] Support Python 3.11 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 5 +++-- setup.cfg | 1 + tox.ini | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 199d2e6..c118faa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10"] + python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10", "3.11-dev"] os: [ubuntu-latest] steps: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9fd1e6d..b09254b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.31.1 + rev: v2.32.0 hooks: - id: pyupgrade args: [--py37-plus] @@ -35,7 +35,7 @@ repos: - id: python-check-blanket-noqa - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.2.0 hooks: - id: check-merge-conflict - id: check-yaml @@ -44,6 +44,7 @@ repos: rev: v1.20.1 hooks: - id: setup-cfg-fmt + args: [--max-py-version=3.11] - repo: https://github.com/tox-dev/tox-ini-fmt rev: 0.5.2 diff --git a/setup.cfg b/setup.cfg index a48f245..5fa2a3d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Internet diff --git a/tox.ini b/tox.ini index 48e8339..0f07848 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = lint - py{py3, 310, 39, 38, 37} + py{py3, 311, 310, 39, 38, 37} [testenv] passenv = From dec407d95875beef6c95cae7cc577a2bf1cf02fd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 May 2022 12:23:31 +0300 Subject: [PATCH 065/138] Add final 'Test successful' to simplify PR status check requirements --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c118faa..6a63739 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,3 +44,11 @@ jobs: with: flags: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} + + success: + needs: test + runs-on: ubuntu-latest + name: test successful + steps: + - name: Success + run: echo Test successful From afbafe1e764d88c2148730fb19d1cba7144f7a25 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 May 2022 15:06:07 +0300 Subject: [PATCH 066/138] Fix test --- tests/test_track.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_track.py b/tests/test_track.py index 3430e29..70d2044 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -110,13 +110,13 @@ class TestPyLastTrack(TestPyLastWithLastFm): def test_track_get_duration(self) -> None: # Arrange - track = pylast.Track("Cher", "Believe", self.network) + track = pylast.Track("Radiohead", "Creep", self.network) # Act duration = track.get_duration() # Assert - assert duration >= 200000 + assert duration >= 100000 def test_track_get_album(self) -> None: # Arrange From ea421db602b39bff2f8bdcfb50c704cc84827210 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 2 May 2022 12:00:21 +0000 Subject: [PATCH 067/138] chore(deps): add renovate.json --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..f45d8f1 --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} From 861182253c77473c2493df5e86d685b84f49635a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 May 2022 15:13:39 +0300 Subject: [PATCH 068/138] Move to .github and add labels --- .github/renovate.json | 9 +++++++++ .pre-commit-config.yaml | 1 + renovate.json | 5 ----- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .github/renovate.json delete mode 100644 renovate.json diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..b5aba8d --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,9 @@ +{ + "extends": [ + "config:base" + ], + "labels": [ + "changelog: skip", + "dependencies" + ] +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b09254b..ee0845c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,6 +38,7 @@ repos: rev: v4.2.0 hooks: - id: check-merge-conflict + - id: check-json - id: check-yaml - repo: https://github.com/asottile/setup-cfg-fmt diff --git a/renovate.json b/renovate.json deleted file mode 100644 index f45d8f1..0000000 --- a/renovate.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": [ - "config:base" - ] -} From 4fc4a6ad8992a39df4e0e42a24a3d5279c2471cc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 2 May 2022 18:10:25 +0300 Subject: [PATCH 069/138] Allow combining major bumps for GHA --- .github/renovate.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/renovate.json b/.github/renovate.json index b5aba8d..92fd789 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -5,5 +5,12 @@ "labels": [ "changelog: skip", "dependencies" + ], + "packageRules": [ + { + "groupName": "github-actions", + "matchManagers": ["github-actions"], + "separateMajorMinor": "false" + } ] } From 75e2dd5f2e709ca91cbcbf734b376a7e7e7989ee Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 2 May 2022 15:20:10 +0000 Subject: [PATCH 070/138] chore(deps): update github-actions to v3 --- .github/workflows/deploy.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5002205..b0f5815 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index c22c0d0..95156ef 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -12,7 +12,7 @@ jobs: sync: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: micnncim/action-label-syncer@v1 with: prune: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b095963..649ca66 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,6 +7,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - uses: pre-commit/action@v2.0.3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a63739..bdc8aa1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: os: [ubuntu-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 @@ -40,7 +40,7 @@ jobs: PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }} - name: Upload coverage - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: flags: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From 3823d77a35365331672721b508e3b58abc90520d Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 5 Jun 2022 20:27:34 +0000 Subject: [PATCH 071/138] chore(deps): update pre-commit/action action to v3 --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 649ca66..bd2ec4f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,4 +9,4 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 - - uses: pre-commit/action@v2.0.3 + - uses: pre-commit/action@v3.0.0 From 139e77707d07540fbd5f2a8f977edfba812623d5 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 8 Jun 2022 16:08:22 +0000 Subject: [PATCH 072/138] chore(deps): update actions/setup-python action to v4 --- .github/workflows/deploy.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b0f5815..f33fa85 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.10" cache: pip diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bd2ec4f..3ded542 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,5 +8,5 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v4 - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bdc8aa1..bf5d9ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: pip From 1e9d7d8c9434086e8a04443da45170906a71cfea Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Jun 2022 19:55:48 +0300 Subject: [PATCH 073/138] A Python version is required for v4 --- .github/workflows/lint.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3ded542..c78a405 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,4 +9,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + with: + python-version: "3.x" - uses: pre-commit/action@v3.0.0 From 7df369dfff631d672ed38baf03b468128096fe83 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 16:53:11 +0000 Subject: [PATCH 074/138] chore(deps): update mheap/github-action-required-labels action to v2 --- .github/workflows/require-pr-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/require-pr-label.yml b/.github/workflows/require-pr-label.yml index a2c74d5..1079f3f 100644 --- a/.github/workflows/require-pr-label.yml +++ b/.github/workflows/require-pr-label.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v1 + - uses: mheap/github-action-required-labels@v2 with: mode: minimum count: 1 From aeba21dedb7a5465d2fac9f81cb099c21f81d7eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Jul 2022 17:40:19 +0000 Subject: [PATCH 075/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.32.0 → v2.34.0](https://github.com/asottile/pyupgrade/compare/v2.32.0...v2.34.0) - [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0) - [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ee0845c..2b9ebc7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.34.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black args: [--target-version=py37] @@ -35,7 +35,7 @@ repos: - id: python-check-blanket-noqa - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: check-merge-conflict - id: check-json From 8a967b52f4c1c7b1d031aff642ed93b5939f072a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 25 Jul 2022 21:21:03 +0300 Subject: [PATCH 076/138] Replace deprecated pypa/gh-action-pypi-publish@master with @release/v1 Committed via https://github.com/asottile/all-repos --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f33fa85..c2af694 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,13 +39,13 @@ jobs: - name: Publish package to PyPI if: github.event.action == 'published' - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.pypi_password }} - name: Publish package to TestPyPI - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.test_pypi_password }} From 7f1de76f6e1e8832381d87950951cb8a4579ec0e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 26 Sep 2022 11:30:50 +0300 Subject: [PATCH 077/138] Fix test_track_get_duration --- tests/test_track.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_track.py b/tests/test_track.py index 70d2044..dae2c9c 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Integration (not unit) tests for pylast.py """ @@ -110,7 +109,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): def test_track_get_duration(self) -> None: # Arrange - track = pylast.Track("Radiohead", "Creep", self.network) + track = pylast.Track("Daft Punk", "Something About Us", self.network) # Act duration = track.get_duration() From 98943d606e179772f1254ec56e5ef5c552ceef30 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 26 Sep 2022 11:04:45 +0300 Subject: [PATCH 078/138] Migrate from setup.* to pyproject.toml --- .flake8 | 2 + .github/workflows/deploy.yml | 5 +-- .github/workflows/test.yml | 4 +- .pre-commit-config.yaml | 25 +++++++----- pyproject.toml | 73 ++++++++++++++++++++++++++++++++++++ setup.cfg | 58 ---------------------------- setup.py | 12 ------ tox.ini | 1 + 8 files changed, 96 insertions(+), 84 deletions(-) create mode 100644 .flake8 create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..f4546ad --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max_line_length = 88 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c2af694..0c72ce1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,9 +22,9 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.x" cache: pip - cache-dependency-path: setup.cfg + cache-dependency-path: pyproject.toml - name: Install dependencies run: | @@ -33,7 +33,6 @@ jobs: - name: Build package run: | - python setup.py --version python -m build twine check --strict dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf5d9ba..e0d36a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: pip - cache-dependency-path: setup.cfg + cache-dependency-path: pyproject.toml - name: Install dependencies run: | @@ -48,7 +48,7 @@ jobs: success: needs: test runs-on: ubuntu-latest - name: test successful + name: Test successful steps: - name: Success run: echo Test successful diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b9ebc7..d076c82 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.38.2 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black args: [--target-version=py37] @@ -16,7 +16,7 @@ repos: hooks: - id: blacken-docs args: [--target-version=py37] - additional_dependencies: [black==21.12b0] + additional_dependencies: [black==22.8.0] - repo: https://github.com/PyCQA/isort rev: 5.10.1 @@ -24,7 +24,7 @@ repos: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] @@ -37,15 +37,22 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 hooks: - - id: check-merge-conflict - id: check-json + - id: check-merge-conflict + - id: check-toml - id: check-yaml + - id: end-of-file-fixer + - id: requirements-txt-fixer - - repo: https://github.com/asottile/setup-cfg-fmt - rev: v1.20.1 + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 0.3.5 hooks: - - id: setup-cfg-fmt - args: [--max-py-version=3.11] + - id: pyproject-fmt + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.10.1 + hooks: + - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt rev: 0.5.2 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3e9af83 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61.2", + "setuptools-scm", +] + +[project] +name = "pylast" +description = "A Python interface to Last.fm and Libre.fm" +readme = "README.md" +keywords = [ + "Last.fm", + "music", + "scrobble", + "scrobbling", +] +license = {text = "Apache-2.0"} +maintainers = [{name = "Hugo van Kemenade"}] +authors = [{name = "Amr Hassan and Contributors", email = "amr.hassan@gmail.com"}] +requires-python = ">=3.7" +dependencies = [ + "httpx", + 'importlib-metadata; python_version < "3.8"', +] +dynamic = [ + "version", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Internet", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Software Development :: Libraries :: Python Modules", +] +[project.optional-dependencies] +tests = [ + "flaky", + "pytest", + "pytest-cov", + "pytest-random-order", + "pyyaml", +] + +[project.urls] +Changelog = "https://github.com/pylast/pylast/releases" +Homepage = "https://github.com/pylast/pylast" +Source = "https://github.com/pylast/pylast" + + +[tool.isort] +profile = "black" + +[tool.setuptools] +package-dir = {"" = "src"} +license-files = ["LICENSE.txt"] +include-package-data = false + +[tool.setuptools.packages.find] +where = ["src"] +namespaces = false + +[tool.setuptools_scm] +local_scheme = "no-local-version" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 5fa2a3d..0000000 --- a/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -[metadata] -name = pylast -description = A Python interface to Last.fm and Libre.fm -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/pylast/pylast -author = Amr Hassan and Contributors -author_email = amr.hassan@gmail.com -maintainer = Hugo van Kemenade -license = Apache-2.0 -license_file = LICENSE.txt -classifiers = - Development Status :: 5 - Production/Stable - License :: OSI Approved :: Apache Software License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python :: Implementation :: PyPy - Topic :: Internet - Topic :: Multimedia :: Sound/Audio - Topic :: Software Development :: Libraries :: Python Modules -keywords = - Last.fm - music - scrobble - scrobbling - -[options] -packages = find: -install_requires = - httpx - importlib-metadata;python_version < '3.8' -python_requires = >=3.7 -package_dir = =src -setup_requires = - setuptools-scm - -[options.packages.find] -where = src - -[options.extras_require] -tests = - flaky - pytest - pytest-cov - pytest-random-order - pyyaml - -[flake8] -max_line_length = 88 - -[tool:isort] -profile = black diff --git a/setup.py b/setup.py deleted file mode 100755 index 630d39e..0000000 --- a/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import setup - - -def local_scheme(version: str) -> str: - """Skip the local version (eg. +xyz of 0.6.1.dev4+gdf99fe2) - to be able to upload to Test PyPI""" - return "" - - -setup( - use_scm_version={"local_scheme": local_scheme}, -) diff --git a/tox.ini b/tox.ini index 0f07848..a06262e 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = lint py{py3, 311, 310, 39, 38, 37} +isolated_build = true [testenv] passenv = From fc288040a82497bb80bb0e492f2f1f6571fa8b73 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 26 Sep 2022 14:01:16 +0300 Subject: [PATCH 079/138] Migrate from setuptools + setuptools_scm to hatchling + hatch-vcs --- pyproject.toml | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e9af83..461b858 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ [build-system] -build-backend = "setuptools.build_meta" +build-backend = "hatchling.build" requires = [ - "setuptools>=61.2", - "setuptools-scm", + "hatch-vcs", + "hatchling", ] [project] @@ -57,17 +57,11 @@ Homepage = "https://github.com/pylast/pylast" Source = "https://github.com/pylast/pylast" +[tool.hatch] +version.source = "vcs" + +[tool.hatch.version.raw-options] +local_scheme = "no-local-version" + [tool.isort] profile = "black" - -[tool.setuptools] -package-dir = {"" = "src"} -license-files = ["LICENSE.txt"] -include-package-data = false - -[tool.setuptools.packages.find] -where = ["src"] -namespaces = false - -[tool.setuptools_scm] -local_scheme = "no-local-version" From dbbbcfec44b39635b6fd9cd670ea4ba908737909 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 26 Sep 2022 14:20:35 +0300 Subject: [PATCH 080/138] pyLast 5.1+ supports Python 3.7-3.11 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a2a667..baa3cb3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PyPI downloads](https://img.shields.io/pypi/dm/pylast.svg)](https://pypistats.org/packages/pylast) [![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions) [![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/main/graph/badge.svg)](https://codecov.io/gh/pylast/pylast) -[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Code style: Black](https://img.shields.io/badge/code%20style-Black-000000.svg)](https://github.com/psf/black) [![DOI](https://zenodo.org/badge/7803088.svg)](https://zenodo.org/badge/latestdoi/7803088) A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites @@ -35,6 +35,7 @@ Or from requirements.txt: Note: +* pyLast 5.1+ supports Python 3.7-3.11. * pyLast 5.0+ supports Python 3.7-3.10. * pyLast 4.3+ supports Python 3.6-3.10. * pyLast 4.0 - 4.2 supports Python 3.6-3.9. From 41e0dd604ef61b06c79ff6cc88972bac2430d4bd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Oct 2022 17:43:21 +0000 Subject: [PATCH 081/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.34.0 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.38.2) - [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0) - [github.com/PyCQA/flake8: 4.0.1 → 5.0.4](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.4) - [github.com/asottile/setup-cfg-fmt: v1.20.1 → v2.0.0](https://github.com/asottile/setup-cfg-fmt/compare/v1.20.1...v2.0.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d076c82..578769b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.38.2 + rev: v3.1.0 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black args: [--target-version=py37] @@ -16,7 +16,7 @@ repos: hooks: - id: blacken-docs args: [--target-version=py37] - additional_dependencies: [black==22.8.0] + additional_dependencies: [black==22.10.0] - repo: https://github.com/PyCQA/isort rev: 5.10.1 From 7f3518fc1a760cf6efa8d4a56986c92f769d23fb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 25 Oct 2022 18:32:50 +0300 Subject: [PATCH 082/138] Test on Python 3.11 final --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0d36a9..d566e4c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10", "3.11-dev"] + python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest] steps: From d03e25fc6c8a49c0fa8306ec2dcd19f938fc9837 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 9 Nov 2022 13:44:07 +0200 Subject: [PATCH 083/138] Test Python 3.12-dev Committed via https://github.com/asottile/all-repos --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d566e4c..280b767 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] os: [ubuntu-latest] steps: From 8169fad09db34d0b52e32365241aa1b8143f3417 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 Dec 2022 21:56:31 +0000 Subject: [PATCH 084/138] chore(deps): update mheap/github-action-required-labels action to v3 --- .github/workflows/require-pr-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/require-pr-label.yml b/.github/workflows/require-pr-label.yml index 1079f3f..d243fa6 100644 --- a/.github/workflows/require-pr-label.yml +++ b/.github/workflows/require-pr-label.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v2 + - uses: mheap/github-action-required-labels@v3 with: mode: minimum count: 1 From 7861fd55bdd412b30096641787598b0e22918625 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 18:09:58 +0000 Subject: [PATCH 085/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.1.0 → v3.3.1](https://github.com/asottile/pyupgrade/compare/v3.1.0...v3.3.1) - [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0) - [github.com/PyCQA/isort: 5.10.1 → 5.11.4](https://github.com/PyCQA/isort/compare/5.10.1...5.11.4) - [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0) - [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0) - [github.com/tox-dev/pyproject-fmt: 0.3.5 → 0.4.1](https://github.com/tox-dev/pyproject-fmt/compare/0.3.5...0.4.1) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 578769b..3c7c47d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.3.1 hooks: - id: pyupgrade args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.12.0 hooks: - id: black args: [--target-version=py37] @@ -19,12 +19,12 @@ repos: additional_dependencies: [black==22.10.0] - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.11.4 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.0.0 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] @@ -35,7 +35,7 @@ repos: - id: python-check-blanket-noqa - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-json - id: check-merge-conflict @@ -45,7 +45,7 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.3.5 + rev: 0.4.1 hooks: - id: pyproject-fmt From a37ac22e6c74c41a6b88e26ef55b0654432debb5 Mon Sep 17 00:00:00 2001 From: ndm13 Date: Mon, 2 Jan 2023 15:09:16 -0500 Subject: [PATCH 086/138] Add code from pylast #407 to readme Describe authentication with OAuth token --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index baa3cb3..8676805 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,32 @@ network = pylast.LastFMNetwork( password_hash=password_hash, ) +# You can also authenticate with a session key +SESSION_KEY_FILE = os.path.join(os.path.expanduser("~"), ".session_key") +network = pylast.LastFMNetwork(API_KEY, API_SECRET) +if not os.path.exists(SESSION_KEY_FILE): + skg = pylast.SessionKeyGenerator(network) + url = skg.get_web_auth_url() + + print(f"Please authorize this script to access your account: {url}\n") + import time + import webbrowser + + webbrowser.open(url) + + while True: + try: + session_key = skg.get_web_auth_session_key(url) + with open(SESSION_KEY_FILE, "w") as f: + f.write(session_key) + break + except pylast.WSError: + time.sleep(1) +else: + session_key = open(SESSION_KEY_FILE).read() + +network.session_key = session_key + # Now you can use that object everywhere track = network.get_track("Iron Maiden", "The Nomad") track.love() From 8647cbdd4844dd80ab9762fc8e18c0cf225b2cc9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 5 Jan 2023 18:36:38 +0200 Subject: [PATCH 087/138] Make alternative clearer via own code blocks --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8676805..4c624ba 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,12 @@ network = pylast.LastFMNetwork( username=username, password_hash=password_hash, ) +``` -# You can also authenticate with a session key +Alternatively, instead of creating `network` with a username and password, +you can authenticate with a session key: + +```python SESSION_KEY_FILE = os.path.join(os.path.expanduser("~"), ".session_key") network = pylast.LastFMNetwork(API_KEY, API_SECRET) if not os.path.exists(SESSION_KEY_FILE): @@ -110,7 +114,11 @@ else: session_key = open(SESSION_KEY_FILE).read() network.session_key = session_key +``` +And away we go: + +```python # Now you can use that object everywhere track = network.get_track("Iron Maiden", "The Nomad") track.love() @@ -120,6 +128,7 @@ track.add_tags(("awesome", "favorite")) # to get more help about anything and see examples of how it works ``` + More examples in hugovk/lastfm-tools and [tests/](https://github.com/pylast/pylast/tree/main/tests). From 28403386a8438722226ece7081aac5944575ffc1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 5 Jan 2023 18:41:16 +0200 Subject: [PATCH 088/138] Bump Black --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3c7c47d..405505c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: blacken-docs args: [--target-version=py37] - additional_dependencies: [black==22.10.0] + additional_dependencies: [black==22.12.0] - repo: https://github.com/PyCQA/isort rev: 5.11.4 From e63ecc7bea1bd4412d31736bf4266f9ce52afbda Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 5 Jan 2023 18:42:43 +0200 Subject: [PATCH 089/138] Autolabel pre-commit PRs with 'changelog: skip' --- .github/release-drafter.yml | 5 +++++ .github/workflows/release-drafter.yml | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 67eccf9..ba26220 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -22,6 +22,11 @@ categories: exclude-labels: - "changelog: skip" +autolabeler: + - label: "changelog: skip" + branch: + - "/pre-commit-ci-update-config/" + template: | $CHANGES diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index cb11924..3f24b79 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -5,11 +5,27 @@ on: # branches to consider in the event; optional, defaults to all branches: - main + # pull_request event is required only for autolabeler + pull_request: + # Only following types are handled by the action, but one can default to all as well + types: [opened, reopened, synchronize] + # pull_request_target event is required for autolabeler to support PRs from forks + # pull_request_target: + # types: [opened, reopened, synchronize] workflow_dispatch: +permissions: + contents: read + jobs: update_release_draft: if: github.repository_owner == 'pylast' + permissions: + # write permission is required to create a GitHub Release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" From f5ea06c6c9488b0f6ffeee52374f197f7a5d7224 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 6 Jan 2023 09:49:52 +0200 Subject: [PATCH 090/138] Include "import pylast" in both blocks --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4c624ba..0739ee7 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ Alternatively, instead of creating `network` with a username and password, you can authenticate with a session key: ```python +import pylast + SESSION_KEY_FILE = os.path.join(os.path.expanduser("~"), ".session_key") network = pylast.LastFMNetwork(API_KEY, API_SECRET) if not os.path.exists(SESSION_KEY_FILE): From dab0a5b6614b5cdc9b9fd73fd47e81795377c671 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 29 Jan 2023 13:27:16 +0200 Subject: [PATCH 091/138] Bump isort to fix Poetry Re: https://github.com/PyCQA/isort/pull/2078 Committed via https://github.com/asottile/all-repos --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 405505c..86f2f3e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: additional_dependencies: [black==22.12.0] - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort From 15f0ccfd58117bee8e04f41eabd4b53fa9e7ba4e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 19 Mar 2023 15:53:12 +0200 Subject: [PATCH 092/138] Replace deprecated repository_url with repository-url Committed via https://github.com/asottile/all-repos --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0c72ce1..95ae3da 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -48,4 +48,4 @@ jobs: with: user: __token__ password: ${{ secrets.test_pypi_password }} - repository_url: https://test.pypi.org/legacy/ + repository-url: https://test.pypi.org/legacy/ From 111334328e584875a71a789074a0fe5c2f54cf43 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 2 Apr 2023 09:06:18 +0000 Subject: [PATCH 093/138] chore(deps): update mheap/github-action-required-labels action to v4 --- .github/workflows/require-pr-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/require-pr-label.yml b/.github/workflows/require-pr-label.yml index d243fa6..3b997b2 100644 --- a/.github/workflows/require-pr-label.yml +++ b/.github/workflows/require-pr-label.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v3 + - uses: mheap/github-action-required-labels@v4 with: mode: minimum count: 1 From 6a7a23cd9a508a7e1ca7c48fc4f5ffa7da39930b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 19:10:46 +0000 Subject: [PATCH 094/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.12.0 → 23.3.0](https://github.com/psf/black/compare/22.12.0...23.3.0) - [github.com/asottile/blacken-docs: v1.12.1 → 1.13.0](https://github.com/asottile/blacken-docs/compare/v1.12.1...1.13.0) - [github.com/pre-commit/pygrep-hooks: v1.9.0 → v1.10.0](https://github.com/pre-commit/pygrep-hooks/compare/v1.9.0...v1.10.0) - [github.com/tox-dev/pyproject-fmt: 0.4.1 → 0.9.2](https://github.com/tox-dev/pyproject-fmt/compare/0.4.1...0.9.2) - [github.com/abravalheri/validate-pyproject: v0.10.1 → v0.12.2](https://github.com/abravalheri/validate-pyproject/compare/v0.10.1...v0.12.2) - [github.com/tox-dev/tox-ini-fmt: 0.5.2 → 1.0.0](https://github.com/tox-dev/tox-ini-fmt/compare/0.5.2...1.0.0) --- .pre-commit-config.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86f2f3e..41b6873 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,13 +6,13 @@ repos: args: [--py37-plus] - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.3.0 hooks: - id: black args: [--target-version=py37] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.1 + rev: 1.13.0 hooks: - id: blacken-docs args: [--target-version=py37] @@ -30,7 +30,7 @@ repos: additional_dependencies: [flake8-2020, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-check-blanket-noqa @@ -45,17 +45,17 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.4.1 + rev: 0.9.2 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.10.1 + rev: v0.12.2 hooks: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.5.2 + rev: 1.0.0 hooks: - id: tox-ini-fmt From 879591e1cc90c997d9d3c940686e255d91dae5bc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 19:10:56 +0000 Subject: [PATCH 095/138] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyproject.toml | 16 +++++++--------- src/pylast/__init__.py | 4 ---- tests/test_pylast.py | 1 - tox.ini | 8 ++++---- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 461b858..141679e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,13 +19,6 @@ license = {text = "Apache-2.0"} maintainers = [{name = "Hugo van Kemenade"}] authors = [{name = "Amr Hassan and Contributors", email = "amr.hassan@gmail.com"}] requires-python = ">=3.7" -dependencies = [ - "httpx", - 'importlib-metadata; python_version < "3.8"', -] -dynamic = [ - "version", -] classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", @@ -42,6 +35,13 @@ classifiers = [ "Topic :: Multimedia :: Sound/Audio", "Topic :: Software Development :: Libraries :: Python Modules", ] +dynamic = [ + "version", +] +dependencies = [ + "httpx", + 'importlib-metadata; python_version < "3.8"', +] [project.optional-dependencies] tests = [ "flaky", @@ -50,13 +50,11 @@ tests = [ "pytest-random-order", "pyyaml", ] - [project.urls] Changelog = "https://github.com/pylast/pylast/releases" Homepage = "https://github.com/pylast/pylast" Source = "https://github.com/pylast/pylast" - [tool.hatch] version.source = "vcs" diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index e152c31..c6cd8be 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -546,7 +546,6 @@ class _Network: context=None, mbid=None, ): - """Used to add a track-play to a user's profile. Parameters: @@ -600,7 +599,6 @@ class _Network: params = {} for i in range(len(tracks_to_scrobble)): - params[f"artist[{i}]"] = tracks_to_scrobble[i]["artist"] params[f"track[{i}]"] = tracks_to_scrobble[i]["title"] @@ -621,7 +619,6 @@ class _Network: } for arg in additional_args: - if arg in tracks_to_scrobble[i] and tracks_to_scrobble[i][arg]: if arg in args_map_to: maps_to = args_map_to[arg] @@ -736,7 +733,6 @@ class LibreFMNetwork(_Network): username: str = "", password_hash: str = "", ) -> None: - super().__init__( name="Libre.fm", homepage="https://libre.fm", diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 7125413..cdaf50e 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -47,7 +47,6 @@ def _no_xfail_rerun_filter(err, name, test, plugin) -> bool: @flaky(max_runs=3, min_passes=1, rerun_filter=_no_xfail_rerun_filter) class TestPyLastWithLastFm(PyLastTestCase): - secrets = None def unix_timestamp(self): diff --git a/tox.ini b/tox.ini index a06262e..de0b9da 100644 --- a/tox.ini +++ b/tox.ini @@ -5,23 +5,23 @@ envlist = isolated_build = true [testenv] +extras = + tests passenv = FORCE_COLOR PYLAST_API_KEY PYLAST_API_SECRET PYLAST_PASSWORD_HASH PYLAST_USERNAME -extras = - tests commands = pytest -v -s -W all --cov pylast --cov tests --cov-report term-missing --cov-report xml --random-order {posargs} [testenv:lint] -passenv = - PRE_COMMIT_COLOR skip_install = true deps = pre-commit +passenv = + PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure From cdb88b9bbbce884865755ead8112155fb230774b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 18 Apr 2023 05:28:30 -0600 Subject: [PATCH 096/138] Update pre-commit --- .pre-commit-config.yaml | 4 ++-- tox.ini | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41b6873..b4d8bf3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: blacken-docs args: [--target-version=py37] - additional_dependencies: [black==22.12.0] + additional_dependencies: [black==23.3.0] - repo: https://github.com/PyCQA/isort rev: 5.12.0 @@ -55,7 +55,7 @@ repos: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.0.0 + rev: 1.3.0 hooks: - id: tox-ini-fmt diff --git a/tox.ini b/tox.ini index de0b9da..7c01999 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,14 @@ [tox] -envlist = +requires = + tox>=4.2 +env_list = lint py{py3, 311, 310, 39, 38, 37} -isolated_build = true [testenv] extras = tests -passenv = +pass_env = FORCE_COLOR PYLAST_API_KEY PYLAST_API_SECRET @@ -20,7 +21,7 @@ commands = skip_install = true deps = pre-commit -passenv = +pass_env = PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure From 9f59dd770c887aeee6da673422b788c53c7d5a4e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 18 Apr 2023 06:04:33 -0600 Subject: [PATCH 097/138] Add support for Python 3.12 --- README.md | 7 ++++--- pyproject.toml | 1 + tox.ini | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0739ee7..ce20f6b 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,10 @@ Or from requirements.txt: Note: -* pyLast 5.1+ supports Python 3.7-3.11. -* pyLast 5.0+ supports Python 3.7-3.10. -* pyLast 4.3+ supports Python 3.6-3.10. +* pyLast 5.2+ supports Python 3.7-3.12. +* pyLast 5.1 supports Python 3.7-3.11. +* pyLast 5.0 supports Python 3.7-3.10. +* pyLast 4.3 - 4.5 supports Python 3.6-3.10. * pyLast 4.0 - 4.2 supports Python 3.6-3.9. * pyLast 3.2 - 3.3 supports Python 3.5-3.8. * pyLast 3.0 - 3.1 supports Python 3.5-3.7. diff --git a/pyproject.toml b/pyproject.toml index 141679e..fc658ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet", diff --git a/tox.ini b/tox.ini index 7c01999..641e732 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 311, 310, 39, 38, 37} + py{py3, 312, 311, 310, 39, 38, 37} [testenv] extras = From 56fc2973717a4694ed8f5901380255f1a304dba2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 18 Apr 2023 06:08:52 -0600 Subject: [PATCH 098/138] Publish to PyPI with a Trusted Publisher --- .github/workflows/deploy.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 95ae3da..3846d7e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,6 +14,10 @@ jobs: if: github.repository_owner == 'pylast' runs-on: ubuntu-latest + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: - uses: actions/checkout@v3 with: @@ -39,13 +43,8 @@ jobs: - name: Publish package to PyPI if: github.event.action == 'published' uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.pypi_password }} - name: Publish package to TestPyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - user: __token__ - password: ${{ secrets.test_pypi_password }} repository-url: https://test.pypi.org/legacy/ From b05b8454f5303050a73724a8207d4c6b9597a5d3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 18 Apr 2023 06:09:25 -0600 Subject: [PATCH 099/138] Update pre-commit --- .pre-commit-config.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4d8bf3..5529b8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,6 @@ repos: rev: 23.3.0 hooks: - id: black - args: [--target-version=py37] - repo: https://github.com/asottile/blacken-docs rev: 1.13.0 @@ -33,12 +32,14 @@ repos: rev: v1.10.0 hooks: - id: python-check-blanket-noqa + - id: python-no-log-warn - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: - - id: check-json + - id: check-case-conflict - id: check-merge-conflict + - id: check-json - id: check-toml - id: check-yaml - id: end-of-file-fixer From dc4bd8474cfe45945120f012a6174561f0fdf9c7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 18 Apr 2023 06:19:54 -0600 Subject: [PATCH 100/138] Test newest PyPy --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 280b767..e3f067c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy-3.8", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] + python-version: ["pypy3.9", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] os: [ubuntu-latest] steps: From 0f59831dc21b51bcf5b4468bc822a84af3968dd5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 3 Jun 2023 17:41:35 +0300 Subject: [PATCH 101/138] Drop support for EOL Python 3.7 --- .github/workflows/test.yml | 3 ++- .pre-commit-config.yaml | 10 +++++----- README.md | 2 +- pyproject.toml | 32 +++++++++++++++----------------- src/pylast/__init__.py | 10 ++-------- tox.ini | 2 +- 6 files changed, 26 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3f067c..3fda04c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.9", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] + python-version: ["pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest] steps: @@ -21,6 +21,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true cache: pip cache-dependency-path: pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5529b8d..0de09d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,9 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.4.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/psf/black rev: 23.3.0 @@ -14,7 +14,7 @@ repos: rev: 1.13.0 hooks: - id: blacken-docs - args: [--target-version=py37] + args: [--target-version=py38] additional_dependencies: [black==23.3.0] - repo: https://github.com/PyCQA/isort @@ -46,12 +46,12 @@ repos: - id: requirements-txt-fixer - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.9.2 + rev: 0.10.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.12.2 + rev: v0.13 hooks: - id: validate-pyproject diff --git a/README.md b/README.md index ce20f6b..3d9e882 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Or from requirements.txt: Note: -* pyLast 5.2+ supports Python 3.7-3.12. +* pyLast 5.2+ supports Python 3.8-3.12. * pyLast 5.1 supports Python 3.7-3.11. * pyLast 5.0 supports Python 3.7-3.10. * pyLast 4.3 - 4.5 supports Python 3.6-3.10. diff --git a/pyproject.toml b/pyproject.toml index fc658ff..d1224dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,30 +18,28 @@ keywords = [ license = {text = "Apache-2.0"} maintainers = [{name = "Hugo van Kemenade"}] authors = [{name = "Amr Hassan and Contributors", email = "amr.hassan@gmail.com"}] -requires-python = ">=3.7" +requires-python = ">=3.8" classifiers = [ - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Internet", - "Topic :: Multimedia :: Sound/Audio", - "Topic :: Software Development :: Libraries :: Python Modules", + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Internet", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = [ "version", ] dependencies = [ "httpx", - 'importlib-metadata; python_version < "3.8"', ] [project.optional-dependencies] tests = [ diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index c6cd8be..60f9b5f 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -23,6 +23,7 @@ from __future__ import annotations import collections import hashlib import html.entities +import importlib.metadata import logging import os import re @@ -36,18 +37,11 @@ from xml.dom import Node, minidom import httpx -try: - # Python 3.8+ - import importlib.metadata as importlib_metadata -except ImportError: - # Python 3.7 and lower - import importlib_metadata # type: ignore - __author__ = "Amr Hassan, hugovk, Mice Pápai" __copyright__ = "Copyright (C) 2008-2010 Amr Hassan, 2013-2021 hugovk, 2017 Mice Pápai" __license__ = "apache2" __email__ = "amr.hassan@gmail.com" -__version__ = importlib_metadata.version(__name__) +__version__ = importlib.metadata.version(__name__) # 1 : This error does not exist diff --git a/tox.ini b/tox.ini index 641e732..b0d3588 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 312, 311, 310, 39, 38, 37} + py{py3, 312, 311, 310, 39, 38} [testenv] extras = From 1c669d8bb01c4274ced602816cf87337731f0d25 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 3 Jun 2023 17:55:00 +0300 Subject: [PATCH 102/138] Fix test: now returns a png --- tests/test_album.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_album.py b/tests/test_album.py index ae2c1a0..a56faf1 100755 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -94,8 +94,8 @@ class TestPyLastAlbum(TestPyLastWithLastFm): image = album.get_cover_image() # Assert - self.assert_startswith(image, "https://") - self.assert_endswith(image, ".gif") + assert image.startswith("https://") + assert image.endswith(".gif") or image.endswith(".png") def test_mbid(self) -> None: # Arrange From 7da76f49bdf4570906459d1ec135460b5863c364 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 3 Jun 2023 18:03:05 +0300 Subject: [PATCH 103/138] Refactor: replace redundant helper methods, no need with pytest --- tests/test_library.py | 4 ++-- tests/test_librefm.py | 6 +++--- tests/test_network.py | 24 ++++++++++++------------ tests/test_pylast.py | 10 +--------- tests/test_user.py | 8 ++++---- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/tests/test_library.py b/tests/test_library.py index e37b771..cc35233 100755 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -16,7 +16,7 @@ class TestPyLastLibrary(TestPyLastWithLastFm): representation = repr(library) # Assert - self.assert_startswith(representation, "pylast.Library(") + assert representation.startswith("pylast.Library(") def test_str(self) -> None: # Arrange @@ -26,7 +26,7 @@ class TestPyLastLibrary(TestPyLastWithLastFm): string = str(library) # Assert - self.assert_endswith(string, "'s Library") + assert string.endswith("'s Library") def test_library_is_hashable(self) -> None: # Arrange diff --git a/tests/test_librefm.py b/tests/test_librefm.py index 0647976..01c43d9 100755 --- a/tests/test_librefm.py +++ b/tests/test_librefm.py @@ -6,11 +6,11 @@ from flaky import flaky import pylast -from .test_pylast import PyLastTestCase, load_secrets +from .test_pylast import load_secrets @flaky(max_runs=3, min_passes=1) -class TestPyLastWithLibreFm(PyLastTestCase): +class TestPyLastWithLibreFm: """Own class for Libre.fm because we don't need the Last.fm setUp""" def test_libre_fm(self) -> None: @@ -38,4 +38,4 @@ class TestPyLastWithLibreFm(PyLastTestCase): representation = repr(network) # Assert - self.assert_startswith(representation, "pylast.LibreFMNetwork(") + assert representation.startswith("pylast.LibreFMNetwork(") diff --git a/tests/test_network.py b/tests/test_network.py index d10cc66..51f0b18 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -330,12 +330,12 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert assert len(images) == 4 - self.assert_startswith(images[pylast.SIZE_SMALL], "https://") - self.assert_endswith(images[pylast.SIZE_SMALL], ".png") + assert images[pylast.SIZE_SMALL].startswith("https://") + assert images[pylast.SIZE_SMALL].endswith(".png") assert "/34s/" in images[pylast.SIZE_SMALL] - self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") - self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") + assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://") + assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png") assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] def test_artist_search(self) -> None: @@ -362,12 +362,12 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert assert len(images) == 5 - self.assert_startswith(images[pylast.SIZE_SMALL], "https://") - self.assert_endswith(images[pylast.SIZE_SMALL], ".png") + assert images[pylast.SIZE_SMALL].startswith("https://") + assert images[pylast.SIZE_SMALL].endswith(".png") assert "/34s/" in images[pylast.SIZE_SMALL] - self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") - self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") + assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://") + assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png") assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] def test_track_search(self) -> None: @@ -396,12 +396,12 @@ class TestPyLastNetwork(TestPyLastWithLastFm): # Assert assert len(images) == 4 - self.assert_startswith(images[pylast.SIZE_SMALL], "https://") - self.assert_endswith(images[pylast.SIZE_SMALL], ".png") + assert images[pylast.SIZE_SMALL].startswith("https://") + assert images[pylast.SIZE_SMALL].endswith(".png") assert "/34s/" in images[pylast.SIZE_SMALL] - self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://") - self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png") + assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://") + assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png") assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] def test_search_get_total_result_count(self) -> None: diff --git a/tests/test_pylast.py b/tests/test_pylast.py index cdaf50e..2c3c7c7 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -32,21 +32,13 @@ def load_secrets(): # pragma: no cover return doc -class PyLastTestCase: - def assert_startswith(self, s, prefix, start=None, end=None) -> None: - assert s.startswith(prefix, start, end) - - def assert_endswith(self, s, suffix, start=None, end=None) -> None: - assert s.endswith(suffix, start, end) - - def _no_xfail_rerun_filter(err, name, test, plugin) -> bool: for _ in test.iter_markers(name="xfail"): return False @flaky(max_runs=3, min_passes=1, rerun_filter=_no_xfail_rerun_filter) -class TestPyLastWithLastFm(PyLastTestCase): +class TestPyLastWithLastFm: secrets = None def unix_timestamp(self): diff --git a/tests/test_user.py b/tests/test_user.py index 6e0f3ba..12308eb 100755 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -24,7 +24,7 @@ class TestPyLastUser(TestPyLastWithLastFm): representation = repr(user) # Assert - self.assert_startswith(representation, "pylast.User('RJ',") + assert representation.startswith("pylast.User('RJ',") def test_str(self) -> None: # Arrange @@ -345,7 +345,7 @@ class TestPyLastUser(TestPyLastWithLastFm): url = user.get_image() # Assert - self.assert_startswith(url, "https://") + assert url.startswith("https://") def test_user_get_library(self) -> None: # Arrange @@ -428,8 +428,8 @@ class TestPyLastUser(TestPyLastWithLastFm): image = user.get_image() # Assert - self.assert_startswith(image, "https://") - self.assert_endswith(image, ".png") + assert image.startswith("https://") + assert image.endswith(".png") def test_get_url(self) -> None: # Arrange From 47eda3ea703de2bce5c701fe8492d1957c60a5b9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 3 Jun 2023 18:10:01 +0300 Subject: [PATCH 104/138] Refactor: make helper methods static --- tests/test_pylast.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 2c3c7c7..4beeed7 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -41,7 +41,8 @@ def _no_xfail_rerun_filter(err, name, test, plugin) -> bool: class TestPyLastWithLastFm: secrets = None - def unix_timestamp(self): + @staticmethod + def unix_timestamp() -> int: return int(time.time()) @classmethod @@ -62,7 +63,8 @@ class TestPyLastWithLastFm: password_hash=password_hash, ) - def helper_is_thing_hashable(self, thing) -> None: + @staticmethod + def helper_is_thing_hashable(thing) -> None: # Arrange things = set() @@ -73,7 +75,8 @@ class TestPyLastWithLastFm: assert thing is not None assert len(things) == 1 - def helper_validate_results(self, a, b, c) -> None: + @staticmethod + def helper_validate_results(a, b, c) -> None: # Assert assert a is not None assert b is not None @@ -97,27 +100,31 @@ class TestPyLastWithLastFm: # Assert self.helper_validate_results(result1, result2, result3) - def helper_at_least_one_thing_in_top_list(self, things, expected_type) -> None: + @staticmethod + def helper_at_least_one_thing_in_top_list(things, expected_type) -> None: # Assert assert len(things) > 1 assert isinstance(things, list) assert isinstance(things[0], pylast.TopItem) assert isinstance(things[0].item, expected_type) - def helper_only_one_thing_in_top_list(self, things, expected_type) -> None: + @staticmethod + def helper_only_one_thing_in_top_list(things, expected_type) -> None: # Assert assert len(things) == 1 assert isinstance(things, list) assert isinstance(things[0], pylast.TopItem) assert isinstance(things[0].item, expected_type) - def helper_only_one_thing_in_list(self, things, expected_type) -> None: + @staticmethod + def helper_only_one_thing_in_list(things, expected_type) -> None: # Assert assert len(things) == 1 assert isinstance(things, list) assert isinstance(things[0], expected_type) - def helper_two_different_things_in_top_list(self, things, expected_type) -> None: + @staticmethod + def helper_two_different_things_in_top_list(things, expected_type) -> None: # Assert assert len(things) == 2 thing1 = things[0] From 02da99f4b0347042d799e5c5becc1a974c667202 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 6 Jun 2023 19:20:56 +0300 Subject: [PATCH 105/138] Use hynek/build-and-inspect-python-package --- .flake8 | 2 +- .github/workflows/deploy.yml | 80 ++++++++++++++++---------- .github/workflows/lint.yml | 3 + .github/workflows/require-pr-label.yml | 3 + 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/.flake8 b/.flake8 index f4546ad..2bcd70e 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,2 @@ [flake8] -max_line_length = 88 +max-line-length = 88 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3846d7e..6594ecf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,49 +2,71 @@ name: Deploy on: push: - branches: - - main + branches: [main] + tags: ["*"] + pull_request: + branches: [main] release: types: - published workflow_dispatch: -jobs: - deploy: - if: github.repository_owner == 'pylast' - runs-on: ubuntu-latest +permissions: + contents: read - permissions: - # IMPORTANT: this permission is mandatory for trusted publishing - id-token: write +jobs: + # Always build & lint package. + build-package: + name: Build & verify package + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v4 + - uses: hynek/build-and-inspect-python-package@v1 + + # Upload to Test PyPI on every commit on main. + release-test-pypi: + name: Publish in-dev package to test.pypi.org + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + needs: build-package + + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + steps: + - name: Download packages built by build-and-inspect-python-package + uses: actions/download-artifact@v3 with: - python-version: "3.x" - cache: pip - cache-dependency-path: pyproject.toml + name: Packages + path: dist - - name: Install dependencies - run: | - python -m pip install -U pip - python -m pip install -U build twine wheel - - - name: Build package - run: | - python -m build - twine check --strict dist/* - - - name: Publish package to PyPI - if: github.event.action == 'published' - uses: pypa/gh-action-pypi-publish@release/v1 - - - name: Publish package to TestPyPI + - name: Upload package to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ + + # Upload to real PyPI on GitHub Releases. + release-pypi: + name: Publish released package to pypi.org + if: github.event.action == 'published' + runs-on: ubuntu-latest + needs: build-package + + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + + steps: + - name: Download packages built by build-and-inspect-python-package + uses: actions/download-artifact@v3 + with: + name: Packages + path: dist + + - name: Upload package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c78a405..477218a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,6 +2,9 @@ name: Lint on: [push, pull_request, workflow_dispatch] +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest diff --git a/.github/workflows/require-pr-label.yml b/.github/workflows/require-pr-label.yml index 3b997b2..2d97091 100644 --- a/.github/workflows/require-pr-label.yml +++ b/.github/workflows/require-pr-label.yml @@ -8,6 +8,9 @@ jobs: label: runs-on: ubuntu-latest + permissions: + issues: write + steps: - uses: mheap/github-action-required-labels@v4 with: From f7a73aa62f0df79efab9ce583f2083d2f4f9333e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 17:29:12 +0000 Subject: [PATCH 106/138] Update mheap/github-action-required-labels action to v5 --- .github/workflows/require-pr-label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/require-pr-label.yml b/.github/workflows/require-pr-label.yml index 2d97091..85c3e3e 100644 --- a/.github/workflows/require-pr-label.yml +++ b/.github/workflows/require-pr-label.yml @@ -12,7 +12,7 @@ jobs: issues: write steps: - - uses: mheap/github-action-required-labels@v4 + - uses: mheap/github-action-required-labels@v5 with: mode: minimum count: 1 From c26c5f86aa4d575ddf5a74a17fdfdc397a30a5a4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 8 Jun 2023 21:22:05 +0300 Subject: [PATCH 107/138] Update on the first day of the month --- .github/renovate.json | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/renovate.json b/.github/renovate.json index 92fd789..2d2f276 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,16 +1,13 @@ { - "extends": [ - "config:base" - ], - "labels": [ - "changelog: skip", - "dependencies" - ], + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:base"], + "labels": ["changelog: skip", "dependencies"], "packageRules": [ { "groupName": "github-actions", "matchManagers": ["github-actions"], "separateMajorMinor": "false" } - ] + ], + "schedule": ["on the first day of the month"] } From 68c0197028dd2fe6b43c423f044026e1a732a4b7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 18 Jun 2023 14:42:29 +0300 Subject: [PATCH 108/138] CI: Replace pypy3.9 with pypy3.10 Committed via https://github.com/asottile/all-repos --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3fda04c..ef202d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["pypy3.10", "3.8", "3.9", "3.10", "3.11", "3.12"] os: [ubuntu-latest] steps: From e4b7af41f936e8fa2c9d63e62c50ed62c9f807a1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 24 Jul 2023 17:33:42 +0300 Subject: [PATCH 109/138] Remove default 'cache-dependency-path: pyproject.toml' Committed via https://github.com/asottile/all-repos --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef202d0..891e2cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip - cache-dependency-path: pyproject.toml + - name: Install dependencies run: | From 74392c4d718b5187004fb8eaf680ae81fadf6816 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 14:22:24 +0300 Subject: [PATCH 110/138] [pre-commit.ci] pre-commit autoupdate (#434) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hugo van Kemenade --- .pre-commit-config.yaml | 17 ++++++++++------- COPYING | 6 +++--- pyproject.toml | 1 - src/pylast/__init__.py | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0de09d9..fe0a791 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.10.1 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 + rev: 1.16.0 hooks: - id: blacken-docs args: [--target-version=py38] @@ -23,7 +23,7 @@ repos: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: [flake8-2020, flake8-implicit-str-concat] @@ -44,19 +44,22 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: requirements-txt-fixer + - id: trailing-whitespace + exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.10.0 + rev: 0.13.1 hooks: - id: pyproject-fmt + additional_dependencies: [tox] - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.13 + rev: v0.14 hooks: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.3.0 + rev: 1.3.1 hooks: - id: tox-ini-fmt diff --git a/COPYING b/COPYING index c4ff845..5b651ea 100644 --- a/COPYING +++ b/COPYING @@ -32,11 +32,11 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: -You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must give any other recipients of the Work or Derivative Works a copy of this License; and -You must cause any modified files to carry prominent notices stating that You changed the files; and +You must cause any modified files to carry prominent notices stating that You changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. diff --git a/pyproject.toml b/pyproject.toml index d1224dd..52292bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ requires-python = ">=3.8" classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 60f9b5f..d901c64 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -1505,7 +1505,7 @@ class _Opus(_Taggable): return f"{self.get_artist().get_name()} - {self.get_title()}" def __eq__(self, other): - if type(self) != type(other): + if type(self) is not type(other): return False a = self.get_title().lower() b = other.get_title().lower() From 47872dbb3288f2230c3d8d20cf18ac126dcdbd8e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 08:37:46 +0300 Subject: [PATCH 111/138] Update actions/checkout action to v4 (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [actions/checkout](https://togithub.com/actions/checkout) | action | major | `v3` -> `v4` | --- ### Release Notes
actions/checkout (actions/checkout) ### [`v4`](https://togithub.com/actions/checkout/blob/HEAD/CHANGELOG.md#v400) [Compare Source](https://togithub.com/actions/checkout/compare/v3...v4) - [Support fetching without the --progress option](https://togithub.com/actions/checkout/pull/1067) - [Update to node20](https://togithub.com/actions/checkout/pull/1436)
--- ### Configuration 📅 **Schedule**: Branch creation - "on the first day of the month" (UTC), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/pylast/pylast). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6594ecf..924a079 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 95156ef..9ca7454 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -12,7 +12,7 @@ jobs: sync: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: micnncim/action-label-syncer@v1 with: prune: false diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 477218a..4cf8d37 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: "3.x" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 891e2cd..2e8f5f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: os: [ubuntu-latest] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 From c0f9f4222a1fd82ae295bafb7528dc73ccc15ec1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:16:48 -0600 Subject: [PATCH 112/138] [pre-commit.ci] pre-commit autoupdate (#437) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe0a791..bb81967 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.10.1 + rev: v3.13.0 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.9.1 hooks: - id: black @@ -48,7 +48,7 @@ repos: exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.13.1 + rev: 1.2.0 hooks: - id: pyproject-fmt additional_dependencies: [tox] From a91bac007d46fbb86d9b27726c293a6d6fb44f56 Mon Sep 17 00:00:00 2001 From: Mia Bilka Date: Thu, 26 Oct 2023 18:02:01 -0700 Subject: [PATCH 113/138] Fixed incorrect docstrings --- src/pylast/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index d901c64..0de19d7 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -1156,7 +1156,7 @@ class _BaseObject: def get_wiki_published_date(self): """ - Returns the summary of the wiki. + Returns the date on which the wiki was published. Only for Album/Track. """ return self.get_wiki("published") @@ -1170,7 +1170,7 @@ class _BaseObject: def get_wiki_content(self): """ - Returns the summary of the wiki. + Returns the content of the wiki. Only for Album/Track. """ return self.get_wiki("content") @@ -1543,7 +1543,7 @@ class _Opus(_Taggable): return self.info["image"][size] def get_title(self, properly_capitalized: bool = False): - """Returns the artist or track title.""" + """Returns the album or track title.""" if properly_capitalized: self.title = _extract( self._request(self.ws_prefix + ".getInfo", True), "name" From b4c8dc728237ff68956e376142bfd57a0d8ddae2 Mon Sep 17 00:00:00 2001 From: Eugene Simonov Date: Fri, 29 Dec 2023 21:00:10 +0200 Subject: [PATCH 114/138] Add type annotations to methods that take timestamp parameter (#442) Co-authored-by: Eugene Simonov Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hugo van Kemenade --- src/pylast/__init__.py | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 0de19d7..c94e714 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -529,25 +529,25 @@ class _Network: def scrobble( self, - artist, - title, - timestamp, - album=None, - album_artist=None, - track_number=None, - duration=None, - stream_id=None, - context=None, - mbid=None, + artist: str, + title: str, + timestamp: int, + album: str | None = None, + album_artist: str | None = None, + track_number: int | None = None, + duration: int | None = None, + stream_id: str | None = None, + context: str | None = None, + mbid: str | None = None, ): """Used to add a track-play to a user's profile. Parameters: artist (Required) : The artist name. title (Required) : The track name. - timestamp (Required) : The time the track started playing, in UNIX + timestamp (Required) : The time the track started playing, in Unix timestamp format (integer number of seconds since 00:00:00, - January 1st 1970 UTC). This must be in the UTC time zone. + January 1st 1970 UTC). album (Optional) : The album name. album_artist (Optional) : The album artist - if this differs from the track artist. @@ -2295,8 +2295,8 @@ class User(_Chartable): self, limit: int = 10, cacheable: bool = True, - time_from=None, - time_to=None, + time_from: int | None = None, + time_to: int | None = None, stream: bool = False, now_playing: bool = False, ): @@ -2307,13 +2307,11 @@ class User(_Chartable): Parameters: limit : If None, it will try to pull all the available data. from (Optional) : Beginning timestamp of a range - only display - scrobbles after this time, in UNIX timestamp format (integer - number of seconds since 00:00:00, January 1st 1970 UTC). This - must be in the UTC time zone. + scrobbles after this time, in Unix timestamp format (integer + number of seconds since 00:00:00, January 1st 1970 UTC). to (Optional) : End timestamp of a range - only display scrobbles - before this time, in UNIX timestamp format (integer number of - seconds since 00:00:00, January 1st 1970 UTC). This must be in - the UTC time zone. + before this time, in Unix timestamp format (integer number of + seconds since 00:00:00, January 1st 1970 UTC). stream: If True, it will yield tracks as soon as a page has been retrieved. This method uses caching. Enable caching only if you're pulling a @@ -2382,7 +2380,7 @@ class User(_Chartable): return _extract(doc, "registered") def get_unixtime_registered(self): - """Returns the user's registration date as a UNIX timestamp.""" + """Returns the user's registration date as a Unix timestamp.""" doc = self._request(self.ws_prefix + ".getInfo", True) From e90d717b66c01d9ddb0dac52d31d1c9a478b11e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 01:18:15 +0000 Subject: [PATCH 115/138] Update github-actions --- .github/workflows/deploy.yml | 6 +++--- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 924a079..3e3174a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,7 +25,7 @@ jobs: with: fetch-depth: 0 - - uses: hynek/build-and-inspect-python-package@v1 + - uses: hynek/build-and-inspect-python-package@v2 # Upload to Test PyPI on every commit on main. release-test-pypi: @@ -40,7 +40,7 @@ jobs: steps: - name: Download packages built by build-and-inspect-python-package - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: Packages path: dist @@ -63,7 +63,7 @@ jobs: steps: - name: Download packages built by build-and-inspect-python-package - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: Packages path: dist diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4cf8d37..f8f9a2b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.x" - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e8f5f4..bf80524 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true From d5fe263c239bdae27d733b90896dceedbf83f29c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 10:31:29 -0700 Subject: [PATCH 116/138] [pre-commit.ci] pre-commit autoupdate (#444) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hugo van Kemenade --- .pre-commit-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bb81967..c5ed648 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v3.13.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [--py38-plus] - - repo: https://github.com/psf/black - rev: 23.9.1 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.12.1 hooks: - id: black @@ -18,7 +18,7 @@ repos: additional_dependencies: [black==23.3.0] - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort @@ -35,7 +35,7 @@ repos: - id: python-no-log-warn - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-case-conflict - id: check-merge-conflict @@ -48,13 +48,13 @@ repos: exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.2.0 + rev: 1.5.3 hooks: - id: pyproject-fmt additional_dependencies: [tox] - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.14 + rev: v0.15 hooks: - id: validate-pyproject From 370ff77f21635dc9579e1ab49b15010a84ffcef8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Jan 2024 09:23:41 +0200 Subject: [PATCH 117/138] Double read timeout to fix 'The read operation timed out' 5 seconds is the default --- src/pylast/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index c94e714..d180a48 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -894,6 +894,7 @@ class _Request: username = "" if username is None else f"?username={username}" (host_name, host_subdir) = self.network.ws_server + timeout = httpx.Timeout(5, read=10) if self.network.is_proxy_enabled(): client = httpx.Client( @@ -901,12 +902,14 @@ class _Request: base_url=f"https://{host_name}", headers=HEADERS, proxies=self.network.proxy, + timeout=timeout, ) else: client = httpx.Client( verify=SSL_CONTEXT, base_url=f"https://{host_name}", headers=HEADERS, + timeout=timeout, ) try: From ffebde28e2948e44ccf94bcec70f2abd528ca425 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 18:59:28 +0000 Subject: [PATCH 118/138] Update github-actions --- .github/workflows/release-drafter.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 3f24b79..0910f73 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -29,6 +29,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next release notes as pull requests are merged into "main" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bf80524..f1cd738 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }} - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: flags: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From 6c888343c81c853b285f96748e79a2230e58c6d8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 4 Feb 2024 12:00:33 -0700 Subject: [PATCH 119/138] Pin codecov/codecov-action to v3.1.5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1cd738..4e94e7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }} - name: Upload coverage - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3.1.5 with: flags: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From 36d89a69e8d88a762806ace12e876e1b5c10054d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 4 Feb 2024 21:06:19 +0200 Subject: [PATCH 120/138] Replace Flake8 with Ruff --- .flake8 | 2 -- .pre-commit-config.yaml | 40 ++++++++++++++-------------------------- pyproject.toml | 27 +++++++++++++++++++++++++-- src/pylast/__init__.py | 12 +++++++----- tests/test_album.py | 2 ++ tests/test_artist.py | 2 ++ tests/test_country.py | 2 ++ tests/test_library.py | 2 ++ tests/test_librefm.py | 2 ++ tests/test_network.py | 3 +++ tests/test_pylast.py | 2 ++ tests/test_tag.py | 2 ++ tests/test_track.py | 3 +++ tests/test_user.py | 2 ++ tests/unicode_test.py | 4 +++- 15 files changed, 71 insertions(+), 36 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 2bcd70e..0000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 88 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c5ed648..5caa25a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.2.0 hooks: - - id: pyupgrade - args: [--py38-plus] + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.1 + rev: 24.1.1 hooks: - id: black @@ -15,24 +15,7 @@ repos: hooks: - id: blacken-docs args: [--target-version=py38] - additional_dependencies: [black==23.3.0] - - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 - hooks: - - id: isort - - - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 - hooks: - - id: flake8 - additional_dependencies: [flake8-2020, flake8-implicit-str-concat] - - - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 - hooks: - - id: python-check-blanket-noqa - - id: python-no-log-warn + additional_dependencies: [black] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 @@ -42,19 +25,19 @@ repos: - id: check-json - id: check-toml - id: check-yaml + - id: debug-statements - id: end-of-file-fixer - - id: requirements-txt-fixer - id: trailing-whitespace exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.5.3 + rev: 1.7.0 hooks: - id: pyproject-fmt additional_dependencies: [tox] - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.15 + rev: v0.16 hooks: - id: validate-pyproject @@ -63,5 +46,10 @@ repos: hooks: - id: tox-ini-fmt + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes + ci: autoupdate_schedule: quarterly diff --git a/pyproject.toml b/pyproject.toml index 52292bb..3413855 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,5 +59,28 @@ version.source = "vcs" [tool.hatch.version.raw-options] local_scheme = "no-local-version" -[tool.isort] -profile = "black" +[tool.ruff.lint] +select = [ + "C4", # flake8-comprehensions + "E", # pycodestyle errors + "EM", # flake8-errmsg + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PGH", # pygrep-hooks + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 +] +extend-ignore = [ + "E203", # Whitespace before ':' + "E221", # Multiple spaces before operator + "E226", # Missing whitespace around arithmetic operator + "E241", # Multiple spaces after ',' +] + +[tool.ruff.lint.isort] +known-first-party = ["pylast"] +required-imports = ["from __future__ import annotations"] diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index d180a48..0396f5b 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -628,7 +628,6 @@ class _Network: class LastFMNetwork(_Network): - """A Last.fm network object api_key: a provided API_KEY @@ -1243,8 +1242,10 @@ class _Chartable(_BaseObject): from_date value to the to_date value. chart_kind should be one of "album", "artist" or "track" """ + import sys + method = ".getWeekly" + chart_kind.title() + "Chart" - chart_type = eval(chart_kind.title()) # string to type + chart_type = getattr(sys.modules[__name__], chart_kind.title()) params = self._get_params() if from_date and to_date: @@ -1356,11 +1357,11 @@ class _Taggable(_BaseObject): new_tags.append(tag) for i in range(0, len(old_tags)): - if not c_old_tags[i] in c_new_tags: + if c_old_tags[i] not in c_new_tags: to_remove.append(old_tags[i]) for i in range(0, len(new_tags)): - if not c_new_tags[i] in c_old_tags: + if c_new_tags[i] not in c_old_tags: to_add.append(new_tags[i]) self.remove_tags(to_remove) @@ -2778,7 +2779,8 @@ def _collect_nodes( main.getAttribute("totalPages") or main.getAttribute("totalpages") ) else: - raise PyLastError("No total pages attribute") + msg = "No total pages attribute" + raise PyLastError(msg) for node in main.childNodes: if not node.nodeType == xml.dom.Node.TEXT_NODE and ( diff --git a/tests/test_album.py b/tests/test_album.py index a56faf1..1146f12 100755 --- a/tests/test_album.py +++ b/tests/test_album.py @@ -2,6 +2,8 @@ """ Integration (not unit) tests for pylast.py """ +from __future__ import annotations + import pylast from .test_pylast import TestPyLastWithLastFm diff --git a/tests/test_artist.py b/tests/test_artist.py index e72474e..d4f9134 100755 --- a/tests/test_artist.py +++ b/tests/test_artist.py @@ -2,6 +2,8 @@ """ Integration (not unit) tests for pylast.py """ +from __future__ import annotations + import pytest import pylast diff --git a/tests/test_country.py b/tests/test_country.py index 6d36ef3..1636b96 100755 --- a/tests/test_country.py +++ b/tests/test_country.py @@ -2,6 +2,8 @@ """ Integration (not unit) tests for pylast.py """ +from __future__ import annotations + import pylast from .test_pylast import TestPyLastWithLastFm diff --git a/tests/test_library.py b/tests/test_library.py index cc35233..592436d 100755 --- a/tests/test_library.py +++ b/tests/test_library.py @@ -2,6 +2,8 @@ """ Integration (not unit) tests for pylast.py """ +from __future__ import annotations + import pylast from .test_pylast import TestPyLastWithLastFm diff --git a/tests/test_librefm.py b/tests/test_librefm.py index 01c43d9..0d9e839 100755 --- a/tests/test_librefm.py +++ b/tests/test_librefm.py @@ -2,6 +2,8 @@ """ Integration (not unit) tests for pylast.py """ +from __future__ import annotations + from flaky import flaky import pylast diff --git a/tests/test_network.py b/tests/test_network.py index 51f0b18..05672d6 100755 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,6 +1,9 @@ """ Integration (not unit) tests for pylast.py """ + +from __future__ import annotations + import re import time diff --git a/tests/test_pylast.py b/tests/test_pylast.py index 4beeed7..c06a9c3 100755 --- a/tests/test_pylast.py +++ b/tests/test_pylast.py @@ -2,6 +2,8 @@ """ Integration (not unit) tests for pylast.py """ +from __future__ import annotations + import os import time diff --git a/tests/test_tag.py b/tests/test_tag.py index 89080f6..7a9675c 100755 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -2,6 +2,8 @@ """ Integration (not unit) tests for pylast.py """ +from __future__ import annotations + import pylast from .test_pylast import TestPyLastWithLastFm diff --git a/tests/test_track.py b/tests/test_track.py index dae2c9c..db9c69c 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -1,6 +1,9 @@ """ Integration (not unit) tests for pylast.py """ + +from __future__ import annotations + import time import pytest diff --git a/tests/test_user.py b/tests/test_user.py index 12308eb..f5069d5 100755 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -2,6 +2,8 @@ """ Integration (not unit) tests for pylast.py """ +from __future__ import annotations + import calendar import datetime as dt import inspect diff --git a/tests/unicode_test.py b/tests/unicode_test.py index bc93dfa..67f234b 100644 --- a/tests/unicode_test.py +++ b/tests/unicode_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import mock import pytest @@ -25,7 +27,7 @@ def test_get_cache_key(artist) -> None: @pytest.mark.parametrize("obj", [pylast.Artist("B\xe9l", mock_network())]) def test_cast_and_hash(obj) -> None: - assert type(str(obj)) is str + assert isinstance(str(obj), str) assert isinstance(hash(obj), int) From 5bccda1102ca7b67c8b92b3fd18733f75df3225b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 4 Feb 2024 22:02:41 +0200 Subject: [PATCH 121/138] Update config --- .github/workflows/deploy.yml | 11 +++++++---- .github/workflows/labels.yml | 3 +++ .github/workflows/lint.yml | 3 +++ .github/workflows/require-pr-label.yml | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3e3174a..8b9a278 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,12 +30,14 @@ jobs: # Upload to Test PyPI on every commit on main. release-test-pypi: name: Publish in-dev package to test.pypi.org - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: | + github.repository_owner == 'pylast' + && github.event_name == 'push' + && github.ref == 'refs/heads/main' runs-on: ubuntu-latest needs: build-package permissions: - # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: @@ -53,12 +55,13 @@ jobs: # Upload to real PyPI on GitHub Releases. release-pypi: name: Publish released package to pypi.org - if: github.event.action == 'published' + if: | + github.repository_owner == 'pylast' + && github.event.action == 'published' runs-on: ubuntu-latest needs: build-package permissions: - # IMPORTANT: this permission is mandatory for trusted publishing id-token: write steps: diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 9ca7454..859c948 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -1,5 +1,8 @@ name: Sync labels +permissions: + pull-requests: write + on: push: branches: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f8f9a2b..dae63b0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,6 +2,9 @@ name: Lint on: [push, pull_request, workflow_dispatch] +env: + FORCE_COLOR: 1 + permissions: contents: read diff --git a/.github/workflows/require-pr-label.yml b/.github/workflows/require-pr-label.yml index 85c3e3e..0d910db 100644 --- a/.github/workflows/require-pr-label.yml +++ b/.github/workflows/require-pr-label.yml @@ -10,6 +10,7 @@ jobs: permissions: issues: write + pull-requests: write steps: - uses: mheap/github-action-required-labels@v5 From 3890cb4c04fa72bd1003ecaa42a8020b78e79e5d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 4 Feb 2024 22:03:26 +0200 Subject: [PATCH 122/138] Add {envpython} and --cov-report html, multiline for clarity --- tox.ini | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b0d3588..e3972c0 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,14 @@ pass_env = PYLAST_PASSWORD_HASH PYLAST_USERNAME commands = - pytest -v -s -W all --cov pylast --cov tests --cov-report term-missing --cov-report xml --random-order {posargs} + {envpython} -m pytest -v -s -W all \ + --cov pylast \ + --cov tests \ + --cov-report html \ + --cov-report term-missing \ + --cov-report xml \ + --random-order \ + {posargs} [testenv:lint] skip_install = true From 77d1b0009c46410057d7b4a563c1c8968dd2ec75 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 29 Dec 2023 21:04:31 +0200 Subject: [PATCH 123/138] Add support for Python 3.13 --- .github/workflows/test.yml | 3 +-- README.md | 1 + pyproject.toml | 1 + tox.ini | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e94e7a..7f09cba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.10", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["pypy3.10", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] os: [ubuntu-latest] steps: @@ -24,7 +24,6 @@ jobs: allow-prereleases: true cache: pip - - name: Install dependencies run: | python -m pip install -U pip diff --git a/README.md b/README.md index 3d9e882..31ea52c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Or from requirements.txt: Note: +* pyLast 5.3+ supports Python 3.8-3.13. * pyLast 5.2+ supports Python 3.8-3.12. * pyLast 5.1 supports Python 3.7-3.11. * pyLast 5.0 supports Python 3.7-3.10. diff --git a/pyproject.toml b/pyproject.toml index 3413855..6857849 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Internet", diff --git a/tox.ini b/tox.ini index e3972c0..3ead5fc 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 312, 311, 310, 39, 38} + py{py3, 313, 312, 311, 310, 39, 38} [testenv] extras = From a14a50a333b0793f5a648384b257c8ed6634a58b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:22:18 +0200 Subject: [PATCH 124/138] Scrutinizer was removed in 2019 --- .scrutinizer.yml | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .scrutinizer.yml diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 43dbfa3..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,9 +0,0 @@ -checks: - python: - code_rating: true - duplicate_code: true -filter: - excluded_paths: - - '*/test/*' -tools: - external_code_coverage: true From f4547a58213dc2520aff5f002d98092f1b285280 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:24:25 +0200 Subject: [PATCH 125/138] Add Prettier to pre-commit --- .pre-commit-config.yaml | 7 +++ CHANGELOG.md | 96 ++++++++++++++++++++++------------------ README.md | 48 ++++++++++---------- RELEASING.md | 7 +-- example_test_pylast.yaml | 8 ++-- 5 files changed, 90 insertions(+), 76 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5caa25a..028e611 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,6 +46,13 @@ repos: hooks: - id: tox-ini-fmt + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + args: [--prose-wrap=always, --print-width=88] + exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md + - repo: meta hooks: - id: check-hooks-apply diff --git a/CHANGELOG.md b/CHANGELOG.md index 2518770..d424974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,117 +12,125 @@ See GitHub Releases: ## Changed -* Fix unsafe creation of temp file for caching, and improve exception raising (#356) @kvanzuijlen -* [pre-commit.ci] pre-commit autoupdate (#362) @pre-commit-ci - +- Fix unsafe creation of temp file for caching, and improve exception raising (#356) + @kvanzuijlen +- [pre-commit.ci] pre-commit autoupdate (#362) @pre-commit-ci ## [4.1.0] - 2021-01-04 + ## Added -* Add support for streaming (#336) @kvanzuijlen -* Add Python 3.9 final to Travis CI (#350) @sheetalsingala +- Add support for streaming (#336) @kvanzuijlen +- Add Python 3.9 final to Travis CI (#350) @sheetalsingala ## Changed -* Update copyright year (#360) @hugovk -* Replace Travis CI with GitHub Actions (#352) @hugovk -* [pre-commit.ci] pre-commit autoupdate (#359) @pre-commit-ci +- Update copyright year (#360) @hugovk +- Replace Travis CI with GitHub Actions (#352) @hugovk +- [pre-commit.ci] pre-commit autoupdate (#359) @pre-commit-ci ## Fixed -* Set limit to 50 by default, not 1 (#355) @hugovk - +- Set limit to 50 by default, not 1 (#355) @hugovk ## [4.0.0] - 2020-10-07 + ## Added -* Add support for Python 3.9 (#347) @hugovk +- Add support for Python 3.9 (#347) @hugovk ## Removed -* Remove deprecated `Artist.get_cover_image`, `User.get_artist_tracks` and `STATUS_TOKEN_ERROR` (#348) @hugovk -* Drop support for EOL Python 3.5 (#346) @hugovk - +- Remove deprecated `Artist.get_cover_image`, `User.get_artist_tracks` and + `STATUS_TOKEN_ERROR` (#348) @hugovk +- Drop support for EOL Python 3.5 (#346) @hugovk ## [3.3.0] - 2020-06-25 + ### Added -* `User.get_now_playing`: Add album and cover image to info (#330) @hugovk +- `User.get_now_playing`: Add album and cover image to info (#330) @hugovk ### Changed -* Improve handling of error responses from the API (#327) @spiritualized +- Improve handling of error responses from the API (#327) @spiritualized ### Deprecated -* Deprecate `Artist.get_cover_image`, they're no longer available from Last.fm (#332) @hugovk +- Deprecate `Artist.get_cover_image`, they're no longer available from Last.fm (#332) + @hugovk ### Fixed -* Fix `artist.get_bio_content()` to return `None` if bio is empty (#326) @hugovk - +- Fix `artist.get_bio_content()` to return `None` if bio is empty (#326) @hugovk ## [3.2.1] - 2020-03-05 + ### Fixed -* Only Python 3 is supported: don't create universal wheel (#318) @hugovk -* Fix regression calling `get_recent_tracks` with `limit=None` (#320) @hugovk -* Fix `DeprecationWarning`: Please use `assertRegex` instead (#323) @hugovk +- Only Python 3 is supported: don't create universal wheel (#318) @hugovk +- Fix regression calling `get_recent_tracks` with `limit=None` (#320) @hugovk +- Fix `DeprecationWarning`: Please use `assertRegex` instead (#323) @hugovk ## [3.2.0] - 2020-01-03 + ### Added -* Support for Python 3.8 -* Store album art URLs when you call `GetTopAlbums` ([#307]) -* Retry paging through results on exception ([#297]) -* More error status codes from https://last.fm/api/errorcodes ([#297]) +- Support for Python 3.8 +- Store album art URLs when you call `GetTopAlbums` ([#307]) +- Retry paging through results on exception ([#297]) +- More error status codes from https://last.fm/api/errorcodes ([#297]) ### Changed -* Respect `get_recent_tracks`' limit when there's a now playing track ([#310]) -* Move installable code to `src/` ([#301]) -* Update `get_weekly_artist_charts` docstring: only for `User` ([#311]) -* Remove Python 2 warnings, `python_requires` should be enough ([#312]) -* Use setuptools_scm to simplify versioning during release ([#316]) -* Various lint and test updates +- Respect `get_recent_tracks`' limit when there's a now playing track ([#310]) +- Move installable code to `src/` ([#301]) +- Update `get_weekly_artist_charts` docstring: only for `User` ([#311]) +- Remove Python 2 warnings, `python_requires` should be enough ([#312]) +- Use setuptools_scm to simplify versioning during release ([#316]) +- Various lint and test updates ### Deprecated -* Last.fm's `user.getArtistTracks` has now been deprecated by Last.fm and is no longer +- Last.fm's `user.getArtistTracks` has now been deprecated by Last.fm and is no longer available. Last.fm returns a "Deprecated - This type of request is no longer supported" error when calling it. A future version of pylast will remove its `User.get_artist_tracks` altogether. ([#305]) -* `STATUS_TOKEN_ERROR` is deprecated and will be removed in a future version. - Use `STATUS_OPERATION_FAILED` instead. +- `STATUS_TOKEN_ERROR` is deprecated and will be removed in a future version. Use + `STATUS_OPERATION_FAILED` instead. ## [3.1.0] - 2019-03-07 + ### Added -* Extract username from session via new +- Extract username from session via new `SessionKeyGenerator.get_web_auth_session_key_username` ([#290]) -* `User.get_track_scrobbles` ([#298]) +- `User.get_track_scrobbles` ([#298]) ### Deprecated -* `User.get_artist_tracks`. Use `User.get_track_scrobbles` as a partial replacement. - ([#298]) +- `User.get_artist_tracks`. Use `User.get_track_scrobbles` as a partial replacement. + ([#298]) ## [3.0.0] - 2019-01-01 + ### Added -* This changelog file ([#273]) + +- This changelog file ([#273]) ### Removed -* Support for Python 2.7 ([#265]) +- Support for Python 2.7 ([#265]) -* Constants `COVER_SMALL`, `COVER_MEDIUM`, `COVER_LARGE`, `COVER_EXTRA_LARGE` - and `COVER_MEGA`. Use `SIZE_SMALL` etc. instead. ([#282]) +- Constants `COVER_SMALL`, `COVER_MEDIUM`, `COVER_LARGE`, `COVER_EXTRA_LARGE` and + `COVER_MEGA`. Use `SIZE_SMALL` etc. instead. ([#282]) ## [2.4.0] - 2018-08-08 + ### Deprecated -* Support for Python 2.7 ([#265]) +- Support for Python 2.7 ([#265]) [4.2.0]: https://github.com/pylast/pylast/compare/4.1.0...4.2.0 [4.1.0]: https://github.com/pylast/pylast/compare/4.0.0...4.1.0 diff --git a/README.md b/README.md index 31ea52c..93bd9b6 100644 --- a/README.md +++ b/README.md @@ -35,31 +35,30 @@ Or from requirements.txt: Note: -* pyLast 5.3+ supports Python 3.8-3.13. -* pyLast 5.2+ supports Python 3.8-3.12. -* pyLast 5.1 supports Python 3.7-3.11. -* pyLast 5.0 supports Python 3.7-3.10. -* pyLast 4.3 - 4.5 supports Python 3.6-3.10. -* pyLast 4.0 - 4.2 supports Python 3.6-3.9. -* pyLast 3.2 - 3.3 supports Python 3.5-3.8. -* pyLast 3.0 - 3.1 supports Python 3.5-3.7. -* pyLast 2.2 - 2.4 supports Python 2.7.10+, 3.4-3.7. -* pyLast 2.0 - 2.1 supports Python 2.7.10+, 3.4-3.6. -* pyLast 1.7 - 1.9 supports Python 2.7, 3.3-3.6. -* pyLast 1.0 - 1.6 supports Python 2.7, 3.3-3.4. -* pyLast 0.5 supports Python 2, 3. -* pyLast < 0.5 supports Python 2. +- pyLast 5.3+ supports Python 3.8-3.13. +- pyLast 5.2+ supports Python 3.8-3.12. +- pyLast 5.1 supports Python 3.7-3.11. +- pyLast 5.0 supports Python 3.7-3.10. +- pyLast 4.3 - 4.5 supports Python 3.6-3.10. +- pyLast 4.0 - 4.2 supports Python 3.6-3.9. +- pyLast 3.2 - 3.3 supports Python 3.5-3.8. +- pyLast 3.0 - 3.1 supports Python 3.5-3.7. +- pyLast 2.2 - 2.4 supports Python 2.7.10+, 3.4-3.7. +- pyLast 2.0 - 2.1 supports Python 2.7.10+, 3.4-3.6. +- pyLast 1.7 - 1.9 supports Python 2.7, 3.3-3.6. +- pyLast 1.0 - 1.6 supports Python 2.7, 3.3-3.4. +- pyLast 0.5 supports Python 2, 3. +- pyLast < 0.5 supports Python 2. ## Features - * Simple public interface. - * Access to all the data exposed by the Last.fm web services. - * Scrobbling support. - * Full object-oriented design. - * Proxy support. - * Internal caching support for some web services calls (disabled by default). - * Support for other API-compatible networks like Libre.fm. - +- Simple public interface. +- Access to all the data exposed by the Last.fm web services. +- Scrobbling support. +- Full object-oriented design. +- Proxy support. +- Internal caching support for some web services calls (disabled by default). +- Support for other API-compatible networks like Libre.fm. ## Getting started @@ -88,8 +87,8 @@ network = pylast.LastFMNetwork( ) ``` -Alternatively, instead of creating `network` with a username and password, -you can authenticate with a session key: +Alternatively, instead of creating `network` with a username and password, you can +authenticate with a session key: ```python import pylast @@ -132,7 +131,6 @@ track.add_tags(("awesome", "favorite")) # to get more help about anything and see examples of how it works ``` - More examples in hugovk/lastfm-tools and [tests/](https://github.com/pylast/pylast/tree/main/tests). diff --git a/RELEASING.md b/RELEASING.md index 9a4d1bc..9b2e38a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,8 +1,8 @@ # Release Checklist - [ ] Get `main` to the appropriate code release state. - [GitHub Actions](https://github.com/pylast/pylast/actions) should be running cleanly for - all merges to `main`. + [GitHub Actions](https://github.com/pylast/pylast/actions) should be running + cleanly for all merges to `main`. [![Test](https://github.com/pylast/pylast/workflows/Test/badge.svg)](https://github.com/pylast/pylast/actions) - [ ] Edit release draft, adjust text if needed: @@ -12,7 +12,8 @@ - [ ] Publish release -- [ ] Check the tagged [GitHub Actions build](https://github.com/pylast/pylast/actions/workflows/deploy.yml) +- [ ] Check the tagged + [GitHub Actions build](https://github.com/pylast/pylast/actions/workflows/deploy.yml) has deployed to [PyPI](https://pypi.org/project/pylast/#history) - [ ] Check installation: diff --git a/example_test_pylast.yaml b/example_test_pylast.yaml index a8fa045..00b09f1 100644 --- a/example_test_pylast.yaml +++ b/example_test_pylast.yaml @@ -1,4 +1,4 @@ -username: TODO_ENTER_YOURS_HERE -password_hash: TODO_ENTER_YOURS_HERE -api_key: TODO_ENTER_YOURS_HERE -api_secret: TODO_ENTER_YOURS_HERE +username: TODO_ENTER_YOURS_HERE +password_hash: TODO_ENTER_YOURS_HERE +api_key: TODO_ENTER_YOURS_HERE +api_secret: TODO_ENTER_YOURS_HERE From d5f1c3d3ac6dac984de011c47aa346616ac1dfe7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:13:50 +0000 Subject: [PATCH 126/138] Update github-actions --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dae63b0..88c0c7c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,4 +17,4 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.x" - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f09cba..9c2c1a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }} - name: Upload coverage - uses: codecov/codecov-action@v3.1.5 + uses: codecov/codecov-action@v4.1.0 with: flags: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From e4d2ebc4a06e598fcaa7254f75868af04dae5753 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 1 Mar 2024 08:02:36 +0200 Subject: [PATCH 127/138] Update test.yml --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c2c1a3..7f09cba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }} - name: Upload coverage - uses: codecov/codecov-action@v4.1.0 + uses: codecov/codecov-action@v3.1.5 with: flags: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From 82cb5048717f460d7da658ff367690e1ab696999 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:16:48 +0000 Subject: [PATCH 128/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.3.4) - [github.com/psf/black-pre-commit-mirror: 24.1.1 → 24.3.0](https://github.com/psf/black-pre-commit-mirror/compare/24.1.1...24.3.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 028e611..89f4d05 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.0 + rev: v0.3.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.1.1 + rev: 24.3.0 hooks: - id: black From fa68aa4ae867ab65f99ba21b5898668fb54ec669 Mon Sep 17 00:00:00 2001 From: Christian McKinnon Date: Wed, 22 May 2024 11:48:48 +0700 Subject: [PATCH 129/138] Update example_test_pylast.yaml Link in README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 93bd9b6..7653b86 100644 --- a/README.md +++ b/README.md @@ -141,9 +141,10 @@ The [tests/](https://github.com/pylast/pylast/tree/main/tests) directory contain integration and unit tests with Last.fm, and plenty of code examples. For integration tests you need a test account at Last.fm that will become cluttered with -test data, and an API key and secret. Either copy -[example_test_pylast.yaml](example_test_pylast.yaml) to test_pylast.yaml and fill out -the credentials, or set them as environment variables like: +test data, and an API key and secret. Either copy [example_test_pylast.yaml]( +https://github.com/pylast/pylast/blob/main/example_test_pylast.yaml) to +test_pylast.yaml and fill out the credentials, or set them as environment +variables like: ```sh export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE From 90c3614d6ac969d471e5e6e8d5d022496fef4cad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 05:10:35 +0000 Subject: [PATCH 130/138] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7653b86..de2987f 100644 --- a/README.md +++ b/README.md @@ -141,10 +141,10 @@ The [tests/](https://github.com/pylast/pylast/tree/main/tests) directory contain integration and unit tests with Last.fm, and plenty of code examples. For integration tests you need a test account at Last.fm that will become cluttered with -test data, and an API key and secret. Either copy [example_test_pylast.yaml]( -https://github.com/pylast/pylast/blob/main/example_test_pylast.yaml) to -test_pylast.yaml and fill out the credentials, or set them as environment -variables like: +test data, and an API key and secret. Either copy +[example_test_pylast.yaml](https://github.com/pylast/pylast/blob/main/example_test_pylast.yaml) +to test_pylast.yaml and fill out the credentials, or set them as environment variables +like: ```sh export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE From 353e32bd6b7eb9cf3cd7be2797d344255df74707 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 22 May 2024 22:26:42 +0300 Subject: [PATCH 131/138] Fix expected result in test --- tests/test_track.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_track.py b/tests/test_track.py index db9c69c..8d8a275 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -140,7 +140,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): # Assert found = False for track in similar: - if str(track.item) == "Madonna - Vogue": + if str(track.item) == "Cher - Strong Enough": found = True break assert found From 35b264bae471cfef1f13663d17ea4777c166240a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 22 May 2024 22:29:05 +0300 Subject: [PATCH 132/138] Refactor --- tests/test_track.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_track.py b/tests/test_track.py index 8d8a275..db04d15 100755 --- a/tests/test_track.py +++ b/tests/test_track.py @@ -138,11 +138,7 @@ class TestPyLastTrack(TestPyLastWithLastFm): similar = track.get_similar() # Assert - found = False - for track in similar: - if str(track.item) == "Cher - Strong Enough": - found = True - break + found = any(str(track.item) == "Cher - Strong Enough" for track in similar) assert found def test_track_get_similar_limits(self) -> None: From 6f97f93dcc9b5f16c0da8a59c42adde9593ee61c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 22 May 2024 22:46:47 +0300 Subject: [PATCH 133/138] Update config --- .github/workflows/lint.yml | 2 ++ .pre-commit-config.yaml | 23 ++++++++++++++++++----- pyproject.toml | 1 + 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 88c0c7c..d553e49 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,6 +4,7 @@ on: [push, pull_request, workflow_dispatch] env: FORCE_COLOR: 1 + PIP_DISABLE_PIP_VERSION_CHECK: 1 permissions: contents: read @@ -17,4 +18,5 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.x" + cache: pip - uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89f4d05..3d54f50 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.4.4 hooks: - id: ruff - args: [--fix, --exit-non-zero-on-fix] + args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.3.0 + rev: 24.4.2 hooks: - id: black @@ -18,8 +18,9 @@ repos: additional_dependencies: [black] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: + - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-json @@ -27,9 +28,21 @@ repos: - id: check-yaml - id: debug-statements - id: end-of-file-fixer + - id: forbid-submodules - id: trailing-whitespace exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.28.4 + hooks: + - id: check-github-workflows + - id: check-renovate + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.0 + hooks: + - id: actionlint + - repo: https://github.com/tox-dev/pyproject-fmt rev: 1.7.0 hooks: @@ -37,7 +50,7 @@ repos: additional_dependencies: [tox] - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.16 + rev: v0.18 hooks: - id: validate-pyproject diff --git a/pyproject.toml b/pyproject.toml index 6857849..9887d1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ select = [ "LOG", # flake8-logging "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) + "RUF022", # unsorted-dunder-all "UP", # pyupgrade "W", # pycodestyle warnings "YTT", # flake8-2020 From aceaa69c9afab935858820491169e251ee2e0b06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:24:06 +0000 Subject: [PATCH 134/138] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.5.0) - [github.com/asottile/blacken-docs: 1.16.0 → 1.18.0](https://github.com/asottile/blacken-docs/compare/1.16.0...1.18.0) - [github.com/python-jsonschema/check-jsonschema: 0.28.4 → 0.28.6](https://github.com/python-jsonschema/check-jsonschema/compare/0.28.4...0.28.6) - [github.com/rhysd/actionlint: v1.7.0 → v1.7.1](https://github.com/rhysd/actionlint/compare/v1.7.0...v1.7.1) - [github.com/tox-dev/pyproject-fmt: 1.7.0 → 2.1.3](https://github.com/tox-dev/pyproject-fmt/compare/1.7.0...2.1.3) --- .pre-commit-config.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d54f50..0895936 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.4 + rev: v0.5.0 hooks: - id: ruff args: [--exit-non-zero-on-fix] @@ -11,7 +11,7 @@ repos: - id: black - repo: https://github.com/asottile/blacken-docs - rev: 1.16.0 + rev: 1.18.0 hooks: - id: blacken-docs args: [--target-version=py38] @@ -33,18 +33,18 @@ repos: exclude: .github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE).md - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.4 + rev: 0.28.6 hooks: - id: check-github-workflows - id: check-renovate - repo: https://github.com/rhysd/actionlint - rev: v1.7.0 + rev: v1.7.1 hooks: - id: actionlint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 1.7.0 + rev: 2.1.3 hooks: - id: pyproject-fmt additional_dependencies: [tox] From 184d0328a97f76572da2e89c0ba8d6c5b2d9eada Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:29:20 +0300 Subject: [PATCH 135/138] Configure pyproject-fmt to add Python 3.13 classifier --- .pre-commit-config.yaml | 1 - pyproject.toml | 63 +++++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0895936..477419b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,6 @@ repos: rev: 2.1.3 hooks: - id: pyproject-fmt - additional_dependencies: [tox] - repo: https://github.com/abravalheri/validate-pyproject rev: v0.18 diff --git a/pyproject.toml b/pyproject.toml index 9887d1f..0586bb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,9 +15,13 @@ keywords = [ "scrobble", "scrobbling", ] -license = {text = "Apache-2.0"} -maintainers = [{name = "Hugo van Kemenade"}] -authors = [{name = "Amr Hassan and Contributors", email = "amr.hassan@gmail.com"}] +license = { text = "Apache-2.0" } +maintainers = [ + { name = "Hugo van Kemenade" }, +] +authors = [ + { name = "Amr Hassan and Contributors", email = "amr.hassan@gmail.com" }, +] requires-python = ">=3.8" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -41,18 +45,16 @@ dynamic = [ dependencies = [ "httpx", ] -[project.optional-dependencies] -tests = [ +optional-dependencies.tests = [ "flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml", ] -[project.urls] -Changelog = "https://github.com/pylast/pylast/releases" -Homepage = "https://github.com/pylast/pylast" -Source = "https://github.com/pylast/pylast" +urls.Changelog = "https://github.com/pylast/pylast/releases" +urls.Homepage = "https://github.com/pylast/pylast" +urls.Source = "https://github.com/pylast/pylast" [tool.hatch] version.source = "vcs" @@ -60,29 +62,36 @@ version.source = "vcs" [tool.hatch.version.raw-options] local_scheme = "no-local-version" -[tool.ruff.lint] -select = [ - "C4", # flake8-comprehensions - "E", # pycodestyle errors - "EM", # flake8-errmsg - "F", # pyflakes errors - "I", # isort - "ISC", # flake8-implicit-str-concat - "LOG", # flake8-logging - "PGH", # pygrep-hooks - "RUF100", # unused noqa (yesqa) +[tool.ruff] +fix = true + +lint.select = [ + "C4", # flake8-comprehensions + "E", # pycodestyle errors + "EM", # flake8-errmsg + "F", # pyflakes errors + "I", # isort + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "PGH", # pygrep-hooks "RUF022", # unsorted-dunder-all - "UP", # pyupgrade - "W", # pycodestyle warnings - "YTT", # flake8-2020 + "RUF100", # unused noqa (yesqa) + "UP", # pyupgrade + "W", # pycodestyle warnings + "YTT", # flake8-2020 ] -extend-ignore = [ +lint.extend-ignore = [ "E203", # Whitespace before ':' "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' ] +lint.isort.known-first-party = [ + "pylast", +] +lint.isort.required-imports = [ + "from __future__ import annotations", +] -[tool.ruff.lint.isort] -known-first-party = ["pylast"] -required-imports = ["from __future__ import annotations"] +[tool.pyproject-fmt] +max_supported_python = "3.13" From c260d7b83fe3938a6163db2d3efd2cd734b060c2 Mon Sep 17 00:00:00 2001 From: Hirad Date: Sun, 7 Jul 2024 09:03:18 +0330 Subject: [PATCH 136/138] change libre.fm to music.lonestar.it --- src/pylast/__init__.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/pylast/__init__.py b/src/pylast/__init__.py index 0396f5b..bd856bc 100644 --- a/src/pylast/__init__.py +++ b/src/pylast/__init__.py @@ -1,6 +1,6 @@ # # pylast - -# A Python interface to Last.fm and Libre.fm +# A Python interface to Last.fm and music.lonestar.it # # Copyright 2008-2010 Amr Hassan # Copyright 2013-2021 hugovk @@ -705,7 +705,7 @@ class LastFMNetwork(_Network): class LibreFMNetwork(_Network): """ - A preconfigured _Network object for Libre.fm + A preconfigured _Network object for music.lonestar.it api_key: a provided API_KEY api_secret: a provided API_SECRET @@ -727,27 +727,27 @@ class LibreFMNetwork(_Network): password_hash: str = "", ) -> None: super().__init__( - name="Libre.fm", - homepage="https://libre.fm", - ws_server=("libre.fm", "/2.0/"), + name="music.lonestar.it", + homepage="https://music.lonestar.it", + ws_server=("music.lonestar.it", "/2.0/"), api_key=api_key, api_secret=api_secret, session_key=session_key, username=username, password_hash=password_hash, domain_names={ - DOMAIN_ENGLISH: "libre.fm", - DOMAIN_GERMAN: "libre.fm", - DOMAIN_SPANISH: "libre.fm", - DOMAIN_FRENCH: "libre.fm", - DOMAIN_ITALIAN: "libre.fm", - DOMAIN_POLISH: "libre.fm", - DOMAIN_PORTUGUESE: "libre.fm", - DOMAIN_SWEDISH: "libre.fm", - DOMAIN_TURKISH: "libre.fm", - DOMAIN_RUSSIAN: "libre.fm", - DOMAIN_JAPANESE: "libre.fm", - DOMAIN_CHINESE: "libre.fm", + DOMAIN_ENGLISH: "music.lonestar.it", + DOMAIN_GERMAN: "music.lonestar.it", + DOMAIN_SPANISH: "music.lonestar.it", + DOMAIN_FRENCH: "music.lonestar.it", + DOMAIN_ITALIAN: "music.lonestar.it", + DOMAIN_POLISH: "music.lonestar.it", + DOMAIN_PORTUGUESE: "music.lonestar.it", + DOMAIN_SWEDISH: "music.lonestar.it", + DOMAIN_TURKISH: "music.lonestar.it", + DOMAIN_RUSSIAN: "music.lonestar.it", + DOMAIN_JAPANESE: "music.lonestar.it", + DOMAIN_CHINESE: "music.lonestar.it", }, urls={ "album": "artist/%(artist)s/album/%(album)s", From 6ae051157fbfd3daa91bcd54663804277cf054ec Mon Sep 17 00:00:00 2001 From: Hirad Date: Sun, 7 Jul 2024 09:16:25 +0330 Subject: [PATCH 137/138] Update README.md --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index de2987f..2b569d3 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,10 @@ Use the pydoc utility for help on usage or see [tests/](tests/) for examples. ## Installation -Install via pip: - -```sh -python3 -m pip install pylast -``` - Install latest development version: ```sh -python3 -m pip install -U git+https://github.com/pylast/pylast +python3 -m pip install -U git+https://git.hirad.it/Hirad/pylast ``` Or from requirements.txt: From 88bb8ea78919d4178e52a07eb45db9e13ed73b1b Mon Sep 17 00:00:00 2001 From: Hirad Date: Sun, 7 Jul 2024 09:16:54 +0330 Subject: [PATCH 138/138] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b569d3..c22fbec 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ python3 -m pip install -U git+https://git.hirad.it/Hirad/pylast Or from requirements.txt: ```txt --e https://github.com/pylast/pylast.git#egg=pylast +-e https://git.hirad.it/Hirad/pylast#egg=pylast ``` Note: