Compare commits

..

No commits in common. "main" and "3.1.0" have entirely different histories.
main ... 3.1.0

40 changed files with 1527 additions and 2100 deletions

View file

@ -11,6 +11,7 @@ charset = utf-8
[*.py] [*.py]
indent_size = 4 indent_size = 4
indent_style = space indent_style = space
trim_trailing_whitespace = true trim_trailing_whitespace = true
# Two-space indentation # Two-space indentation

1
.github/FUNDING.yml vendored
View file

@ -1 +0,0 @@
github: hugovk

View file

@ -4,19 +4,12 @@
### What actually happened? ### What actually happened?
### What versions are you using? ### What versions of OS, Python and pylast are you using?
* OS:
* Python:
* pylast:
Please include **code** that reproduces the issue. Please include **code** that reproduces the issue.
The [best reproductions](https://stackoverflow.com/help/minimal-reproducible-example) The [best reproductions](https://stackoverflow.com/help/mcve) are [self-contained scripts](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/) with minimal dependencies.
are
[self-contained scripts](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/)
with minimal dependencies.
```python ```python
# code goes here code goes here
``` ```

111
.github/labels.yml vendored
View file

@ -1,111 +0,0 @@
# Default GitHub labels
- color: d73a4a
description: "Something isn't working"
name: bug
- color: cfd3d7
description: "This issue or pull request already exists"
name: duplicate
- color: a2eeef
description: "New feature or request"
name: enhancement
- color: 7057ff
description: "Good for newcomers"
name: good first issue
- color: 008672
description: "Extra attention is needed"
name: help wanted
- color: e4e669
description: "This doesn't seem right"
name: invalid
- color: d876e3
description: "Further information is requested"
name: question
- color: ffffff
description: "This will not be worked on"
name: wontfix
# Keep a Changelog labels
# https://keepachangelog.com/en/1.0.0/
- color: 0e8a16
description: "For new features"
name: "changelog: Added"
- color: af99e5
description: "For changes in existing functionality"
name: "changelog: Changed"
- color: FFA500
description: "For soon-to-be removed features"
name: "changelog: Deprecated"
- color: 00A800
description: "For any bug fixes"
name: "changelog: Fixed"
- color: ff0000
description: "For now removed features"
name: "changelog: Removed"
- color: 045aa0
description: "In case of vulnerabilities"
name: "changelog: Security"
- color: fbca04
description: "Exclude PR from release draft"
name: "changelog: skip"
# Other labels
- color: e11d21
description: ""
name: Last.fm bug
- color: FFFFFF
description: ""
name: Milestone-0.3
- color: FFFFFF
description: ""
name: Performance
- color: FFFFFF
description: ""
name: Priority-High
- color: FFFFFF
description: ""
name: Priority-Low
- color: FFFFFF
description: ""
name: Priority-Medium
- color: FFFFFF
description: ""
name: Type-Other
- color: FFFFFF
description: ""
name: Type-Patch
- color: FFFFFF
description: ""
name: Usability
- color: 64c1c0
description: ""
name: backwards incompatible
- color: fef2c0
description: ""
name: build
- color: e99695
description: Feature that will be removed in the future
name: deprecation
- color: FFFFFF
description: ""
name: imported
- color: b60205
description: Removal of a feature, usually done in major releases
name: removal
- color: 0366d6
description: "For dependencies"
name: dependencies
- color: 0052cc
description: "Documentation"
name: docs
- color: f4660e
description: ""
name: Hacktoberfest
- color: f4660e
description: "To credit accepted Hacktoberfest PRs"
name: hacktoberfest-accepted
- color: d65e88
description: "Deploy and release"
name: release
- color: fef2c0
description: "Unit tests, linting, CI, etc."
name: test

View file

@ -1,48 +0,0 @@
name-template: "$RESOLVED_VERSION"
tag-template: "$RESOLVED_VERSION"
categories:
- title: "Added"
labels:
- "changelog: Added"
- "enhancement"
- title: "Changed"
label: "changelog: Changed"
- title: "Deprecated"
label: "changelog: Deprecated"
- title: "Removed"
label: "changelog: Removed"
- title: "Fixed"
labels:
- "changelog: Fixed"
- "bug"
- title: "Security"
label: "changelog: Security"
exclude-labels:
- "changelog: skip"
autolabeler:
- label: "changelog: skip"
branch:
- "/pre-commit-ci-update-config/"
template: |
$CHANGES
version-resolver:
major:
labels:
- "changelog: Removed"
minor:
labels:
- "changelog: Added"
- "changelog: Changed"
- "changelog: Deprecated"
- "enhancement"
patch:
labels:
- "changelog: Fixed"
- "bug"
default: minor

13
.github/renovate.json vendored
View file

@ -1,13 +0,0 @@
{
"$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"]
}

View file

@ -1,75 +0,0 @@
name: Deploy
on:
push:
branches: [main]
tags: ["*"]
pull_request:
branches: [main]
release:
types:
- published
workflow_dispatch:
permissions:
contents: read
jobs:
# Always build & lint package.
build-package:
name: Build & verify package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: hynek/build-and-inspect-python-package@v2
# Upload to Test PyPI on every commit on main.
release-test-pypi:
name: Publish in-dev package to test.pypi.org
if: |
github.repository_owner == 'pylast'
&& github.event_name == 'push'
&& github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
needs: build-package
permissions:
id-token: write
steps:
- name: Download packages built by build-and-inspect-python-package
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
- 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.repository_owner == 'pylast'
&& github.event.action == 'published'
runs-on: ubuntu-latest
needs: build-package
permissions:
id-token: write
steps:
- name: Download packages built by build-and-inspect-python-package
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

View file

@ -1,23 +0,0 @@
name: Sync labels
permissions:
pull-requests: write
on:
push:
branches:
- main
paths:
- .github/labels.yml
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: micnncim/action-label-syncer@v1
with:
prune: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,22 +0,0 @@
name: Lint
on: [push, pull_request, workflow_dispatch]
env:
FORCE_COLOR: 1
PIP_DISABLE_PIP_VERSION_CHECK: 1
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: pip
- uses: pre-commit/action@v3.0.1

View file

@ -1,34 +0,0 @@
name: Release drafter
on:
push:
# 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"
- uses: release-drafter/release-drafter@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,22 +0,0 @@
name: Require PR label
on:
pull_request:
types: [opened, reopened, labeled, unlabeled, synchronize]
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: mheap/github-action-required-labels@v5
with:
mode: minimum
count: 1
labels:
"changelog: Added, changelog: Changed, changelog: Deprecated, changelog:
Fixed, changelog: Removed, changelog: Security, changelog: skip"

View file

@ -1,54 +0,0 @@
name: Test
on: [push, pull_request, workflow_dispatch]
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
python-version: ["pypy3.10", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: pip
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install -U wheel
python -m pip install -U tox
- name: Tox tests
run: |
tox -e py
env:
PYLAST_API_KEY: ${{ secrets.PYLAST_API_KEY }}
PYLAST_API_SECRET: ${{ secrets.PYLAST_API_SECRET }}
PYLAST_PASSWORD_HASH: ${{ secrets.PYLAST_PASSWORD_HASH }}
PYLAST_USERNAME: ${{ secrets.PYLAST_USERNAME }}
- name: Upload coverage
uses: codecov/codecov-action@v3.1.5
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

View file

@ -1,74 +0,0 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.5.0
hooks:
- id: ruff
args: [--exit-non-zero-on-fix]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/asottile/blacken-docs
rev: 1.18.0
hooks:
- id: blacken-docs
args: [--target-version=py38]
additional_dependencies: [black]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-json
- id: check-toml
- 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.6
hooks:
- id: check-github-workflows
- id: check-renovate
- repo: https://github.com/rhysd/actionlint
rev: v1.7.1
hooks:
- id: actionlint
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 2.1.3
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.18
hooks:
- id: validate-pyproject
- repo: https://github.com/tox-dev/tox-ini-fmt
rev: 1.3.1
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
- id: check-useless-excludes
ci:
autoupdate_schedule: quarterly

9
.scrutinizer.yml Normal file
View file

@ -0,0 +1,9 @@
checks:
python:
code_rating: true
duplicate_code: true
filter:
excluded_paths:
- '*/test/*'
tools:
external_code_coverage: true

70
.travis.yml Normal file
View file

@ -0,0 +1,70 @@
language: python
cache: pip
env:
global:
- secure: ivg6II471E9HV8xyqnawLIuP/sZ0J63Y+BC0BQcRVKtLn/K3zmD1ozM3TFL9S549Nxd0FqDKHXJvXsgaTGIDpK8sxE2AMKV5IojyM0iAVuN7YjPK9vwSlRw1u0EysPMFqxOZVQnoDyHrSGIUrP/VMdnhBu6dbUX0FyEkvZshXhY=
- secure: gDWNEYA1EUv4G230/KzcTgcmEST0nf2FeW/z/prsoQBu+TWw1rKKSJAJeMLvuI1z4aYqqNYdmqjWyNhhVK3p5wmFP2lxbhaBT1jDsxxFpePc0nUkdAQOOD0yBpbBGkqkjjxU34HjTX2NFNEbcM3izVVE9oQmS5r4oFFNJgdL91c=
- secure: RpsZblHFU7a5dnkO/JUgi70RkNJwoUh3jJqVo1oOLjL+lvuAmPXhI8MDk2diUk43X+XCBFBEnm7UCGnjUF+hDnobO4T+VrIFuVJWg3C7iKIT+YWvgG6A+CSeo/P0I0dAeUscTr5z4ylOq3EDx4MFSa8DmoWMmjKTAG1GAeTlY2k=
- secure: T5OKyd5Bs0nZbUr+YICbThC5GrFq/kUjX8FokzCv7NWsYaUWIwEmMXXzoYALoB3A+rAglOx6GABaupoNKKg3tFQyxXphuMKpZ8MasMAMFjFW0d7wsgGy0ylhVwrgoKzDbCQ5FKbohC+9ltLs+kKMCQ0L+MI70a/zTfF4/dVWO/o=
- secure: DxBvGGoIgbAeuuU3A6+J1HBbmUAEvqdmK73etw+yNKDLGvvukgTL33dNCr8CZXLKRRvfhrjU7Q01GUpOTxrVQ9nJgsD55kwx0wPtuBWIF80M2m4SPsiVLlwP/LFYD5JMDTDWjFTlVahma8P7qoLjCc7b/RgigWLidH19snQmjdY=
- secure: VPARlWNg/0Nit7a924vJlDfv7yiuTDtrcGZNFrZ6yN3dl8ZjVPizQXQNKA3yq0y2jW25nwjRwZYj3eY5MdM9F7Sw51d+/8AjFtdCuRgDvwlQFR/pCoyzqgJATkXKo7mlejvnA+5EKUzAmu3drIbboFgbLgRTMrG7b/ot9tazTHs=
- secure: CQYL7MH6tSVrCcluIfWfDSTo4E/p+9pF0eI7Vtf0oaZBzyulODHK8h/mzJp4HwezyfOu0RCedq6sloGQr1/29CvWWESaYyoGoGz9Mz2ZS+MpIcjGISfZa+x4vSp6QPFvd4i/1Z/1j2gJVVyswkrIVUwZIDJtfAKzZI5iHx2gH8Y=
- secure: SsKJoJwtDVWrL5xxl9C/gTRy6FhfRQQNNAFOogl9mTs/WeI2t9QTYoKsxLPXOdoRdu4MvT3h/B2sjwggt7zP81fBVxQRTkg4nq0zSHlj0NqclbFa6I5lUYdGwH9gPk/HWJJwXhKRDsqn/iRw2v+qBDs/j3kIgPQ0yjM58LEPXic=
matrix:
include:
- python: 3.6
env: TOXENV=lint
- python: 3.7
env: TOXENV=py37
dist: xenial
- python: 3.6
env: TOXENV=py36
- python: 3.5
env: TOXENV=py35
- python: pypy3
env: TOXENV=pypy3
- python: 3.8-dev
env: TOXENV=py38dev
dist: xenial
allow_failures:
- env: TOXENV=pypy3
fast_finish: true
install:
- travis_retry pip install --upgrade pip
- travis_retry pip install --upgrade tox
- travis_retry pip install --upgrade coverage
script: tox
after_success:
- travis_retry pip install coveralls && coveralls
- travis_retry pip install codecov && codecov
- travis_retry pip install scrutinizer-ocular && ocular
deploy:
- provider: pypi
server: https://test.pypi.org/legacy/
on:
tags: false
repo: pylast/pylast
branch: master
condition: $TOXENV = py37
user: hugovk
password:
secure: "OCNT7Sf7TpS6aKuqBXEWxJZjmEpdERTBp/yllOd9xnpFt2ZL96CyKtAhPA8zu5OP58QFEZSafZRfXYJoz78RDrx3gOdRXCFT00vXIMnjVvrAlieNEHCVAT0kRW9lYK1Cf5baHYsOYIs6EZf2fEAhdzvmh83G4Y1Y+FPR9tA6uy8="
distributions: sdist --format=gztar bdist_wheel
skip_existing: true
- provider: pypi
on:
tags: true
repo: pylast/pylast
branch: master
condition: $TOXENV = py37
user: hugovk
password:
secure: "OCNT7Sf7TpS6aKuqBXEWxJZjmEpdERTBp/yllOd9xnpFt2ZL96CyKtAhPA8zu5OP58QFEZSafZRfXYJoz78RDrx3gOdRXCFT00vXIMnjVvrAlieNEHCVAT0kRW9lYK1Cf5baHYsOYIs6EZf2fEAhdzvmh83G4Y1Y+FPR9tA6uy8="
distributions: sdist --format=gztar bdist_wheel
skip_existing: true

View file

@ -1,159 +1,44 @@
# Changelog # Changelog
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). All notable changes to this project will be documented in this file.
## 4.2.1 and newer 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).
See GitHub Releases:
- https://github.com/pylast/pylast/releases
## [4.2.0] - 2021-03-14
## 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
## [4.1.0] - 2021-01-04
## Added
- 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
## Fixed
- 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
## 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
## [3.3.0] - 2020-06-25
### Added
- `User.get_now_playing`: Add album and cover image to info (#330) @hugovk
### Changed
- 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
### Fixed
- 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
## [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])
### 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
### Deprecated
- 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.
## [3.1.0] - 2019-03-07 ## [3.1.0] - 2019-03-07
### Added ### Added
- Extract username from session via new * Extract username from session via new
`SessionKeyGenerator.get_web_auth_session_key_username` ([#290]) `SessionKeyGenerator.get_web_auth_session_key_username` ([#290])
- `User.get_track_scrobbles` ([#298]) * `User.get_track_scrobbles` ([#298])
### Deprecated ### Deprecated
- `User.get_artist_tracks`. Use `User.get_track_scrobbles` as a partial replacement. * `User.get_artist_tracks`. Use `User.get_track_scrobbles` as a partial replacement.
([#298]) ([#298])
## [3.0.0] - 2019-01-01 ## [3.0.0] - 2019-01-01
### Added ### Added
* This changelog file ([#273])
- This changelog file ([#273])
### Removed ### Removed
- Support for Python 2.7 ([#265]) * Support for Python 2.7 ([#265])
- Constants `COVER_SMALL`, `COVER_MEDIUM`, `COVER_LARGE`, `COVER_EXTRA_LARGE` and * Constants `COVER_SMALL`, `COVER_MEDIUM`, `COVER_LARGE`, `COVER_EXTRA_LARGE`
`COVER_MEGA`. Use `SIZE_SMALL` etc. instead. ([#282]) and `COVER_MEGA`. Use `SIZE_SMALL` etc. instead. ([#282])
## [2.4.0] - 2018-08-08 ## [2.4.0] - 2018-08-08
### Deprecated ### 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 [3.1.0]: https://github.com/pylast/pylast/compare/v3.0.0...3.1.0
[4.0.0]: https://github.com/pylast/pylast/compare/3.3.0...4.0.0
[3.3.0]: https://github.com/pylast/pylast/compare/3.2.1...3.3.0
[3.2.1]: https://github.com/pylast/pylast/compare/3.2.0...3.2.1
[3.2.0]: https://github.com/pylast/pylast/compare/3.1.0...3.2.0
[3.1.0]: https://github.com/pylast/pylast/compare/3.0.0...3.1.0
[3.0.0]: https://github.com/pylast/pylast/compare/2.4.0...3.0.0 [3.0.0]: https://github.com/pylast/pylast/compare/2.4.0...3.0.0
[2.4.0]: https://github.com/pylast/pylast/compare/2.3.0...2.4.0 [2.4.0]: https://github.com/pylast/pylast/compare/2.3.0...2.4.0
[#298]: https://github.com/pylast/pylast/issues/298
[#290]: https://github.com/pylast/pylast/pull/290
[#265]: https://github.com/pylast/pylast/issues/265 [#265]: https://github.com/pylast/pylast/issues/265
[#273]: https://github.com/pylast/pylast/issues/273 [#273]: https://github.com/pylast/pylast/issues/273
[#282]: https://github.com/pylast/pylast/pull/282 [#282]: https://github.com/pylast/pylast/pull/282
[#290]: https://github.com/pylast/pylast/pull/290
[#297]: https://github.com/pylast/pylast/issues/297
[#298]: https://github.com/pylast/pylast/issues/298
[#301]: https://github.com/pylast/pylast/issues/301
[#305]: https://github.com/pylast/pylast/issues/305
[#307]: https://github.com/pylast/pylast/issues/307
[#310]: https://github.com/pylast/pylast/issues/310
[#311]: https://github.com/pylast/pylast/issues/311
[#312]: https://github.com/pylast/pylast/issues/312
[#316]: https://github.com/pylast/pylast/issues/316
[#346]: https://github.com/pylast/pylast/issues/346
[#347]: https://github.com/pylast/pylast/issues/347
[#348]: https://github.com/pylast/pylast/issues/348

View file

@ -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: 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. 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. 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.

4
INSTALL Normal file
View file

@ -0,0 +1,4 @@
Installation Instructions
=========================
Execute "python setup.py install" as a super user.

6
MANIFEST.in Executable file
View file

@ -0,0 +1,6 @@
include pylast/*.py
include setup.py
include README.md
include COPYING
include INSTALL
recursive-include tests *.py

158
README.md
View file

@ -1,65 +1,59 @@
# pyLast pyLast
======
[![PyPI version](https://img.shields.io/pypi/v/pylast.svg)](https://pypi.org/project/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/) [![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) [![Build status](https://travis-ci.org/pylast/pylast.svg?branch=master)](https://travis-ci.org/pylast/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)
[![Coverage (Codecov)](https://codecov.io/gh/pylast/pylast/branch/main/graph/badge.svg)](https://codecov.io/gh/pylast/pylast) [![Coverage (Coveralls)](https://coveralls.io/repos/github/pylast/pylast/badge.svg?branch=master)](https://coveralls.io/github/pylast/pylast?branch=master)
[![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/ambv/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 A Python interface to [Last.fm](https://www.last.fm/) and other API-compatible websites such as [Libre.fm](https://libre.fm/).
such as [Libre.fm](https://libre.fm/).
Use the pydoc utility for help on usage or see [tests/](tests/) for examples. Use the pydoc utility for help on usage or see [tests/](tests/) for examples.
## Installation Installation
------------
Install via pip:
pip install pylast
Install latest development version: Install latest development version:
```sh 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: Or from requirements.txt:
```txt -e git://github.com/pylast/pylast.git#egg=pylast
-e https://git.hirad.it/Hirad/pylast#egg=pylast
```
Note: Note:
- pyLast 5.3+ supports Python 3.8-3.13. * pylast 3.0.0+ supports Python 3.5+ ([#265](https://github.com/pylast/pylast/issues/265))
- pyLast 5.2+ supports Python 3.8-3.12. * pyLast 2.2.0 - 2.4.0 supports Python 2.7.10+, 3.4, 3.5, 3.6, 3.7.
- pyLast 5.1 supports Python 3.7-3.11. * pyLast 2.0.0 - 2.1.0 supports Python 2.7.10+, 3.4, 3.5, 3.6.
- pyLast 5.0 supports Python 3.7-3.10. * pyLast 1.7.0 - 1.9.0 supports Python 2.7, 3.3, 3.4, 3.5, 3.6.
- pyLast 4.3 - 4.5 supports Python 3.6-3.10. * pyLast 1.0.0 - 1.6.0 supports Python 2.7, 3.3, 3.4.
- pyLast 4.0 - 4.2 supports Python 3.6-3.9. * pyLast 0.5 supports Python 2, 3.
- pyLast 3.2 - 3.3 supports Python 3.5-3.8. * pyLast < 0.5 supports Python 2.
- 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 Features
--------
- Simple public interface. * Simple public interface.
- Access to all the data exposed by the Last.fm web services. * Access to all the data exposed by the Last.fm web services.
- Scrobbling support. * Scrobbling support.
- Full object-oriented design. * Full object-oriented design.
- Proxy support. * Proxy support.
- Internal caching support for some web services calls (disabled by default). * Internal caching support for some web services calls (disabled by default).
- Support for other API-compatible networks like Libre.fm. * Support for other API-compatible networks like Libre.fm.
* Python 3-friendly (Starting from 0.5).
## Getting started
Here's some simple code example to get you started. In order to create any object from Getting started
pyLast, you need a `Network` object which represents a social music network that is ---------------
Last.fm or any other API-compatible one. You can obtain a pre-configured one for Last.fm
and use it as follows: 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 Last.fm or any other API-compatible one. You can obtain a pre-configured one for Last.fm and use it as follows:
```python ```python
import pylast import pylast
@ -73,50 +67,14 @@ API_SECRET = "425b55975eed76058ac220b7b4e8a054"
username = "your_user_name" username = "your_user_name"
password_hash = pylast.md5("your_password") password_hash = pylast.md5("your_password")
network = pylast.LastFMNetwork( network = pylast.LastFMNetwork(api_key=API_KEY, api_secret=API_SECRET,
api_key=API_KEY, username=username, password_hash=password_hash)
api_secret=API_SECRET,
username=username,
password_hash=password_hash,
)
```
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):
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
```
And away we go:
```python
# Now you can use that object everywhere # 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") track = network.get_track("Iron Maiden", "The Nomad")
track.love() track.love()
track.add_tags(("awesome", "favorite")) track.add_tags(("awesome", "favorite"))
@ -125,20 +83,14 @@ track.add_tags(("awesome", "favorite"))
# to get more help about anything and see examples of how it works # to get more help about anything and see examples of how it works
``` ```
More examples in More examples in <a href="https://github.com/hugovk/lastfm-tools">hugovk/lastfm-tools</a> and [tests/](tests/).
<a href="https://github.com/hugovk/lastfm-tools">hugovk/lastfm-tools</a> and
[tests/](https://github.com/pylast/pylast/tree/main/tests).
## Testing Testing
-------
The [tests/](https://github.com/pylast/pylast/tree/main/tests) directory contains The [tests/](tests/) directory contains integration and unit tests with Last.fm, and plenty of code examples.
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 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 ```sh
export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE export PYLAST_USERNAME=TODO_ENTER_YOURS_HERE
@ -148,20 +100,17 @@ export PYLAST_API_SECRET=TODO_ENTER_YOURS_HERE
``` ```
To run all unit and integration tests: To run all unit and integration tests:
```sh ```sh
python3 -m pip install -e ".[tests]" pip install pytest flaky mock
pytest pytest
``` ```
Or run just one test case: Or run just one test case:
```sh ```sh
pytest -k test_scrobble pytest -k test_scrobble
``` ```
To run with coverage: To run with coverage:
```sh ```sh
pytest -v --cov pylast --cov-report term-missing pytest -v --cov pylast --cov-report term-missing
coverage report # for command-line report coverage report # for command-line report
@ -169,7 +118,8 @@ coverage html # for HTML report
open htmlcov/index.html open htmlcov/index.html
``` ```
## Logging Logging
-------
To enable from your own code: To enable from your own code:
@ -177,8 +127,7 @@ To enable from your own code:
import logging import logging
import pylast import pylast
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.DEBUG)
network = pylast.LastFMNetwork(...) network = pylast.LastFMNetwork(...)
``` ```
@ -186,8 +135,5 @@ network = pylast.LastFMNetwork(...)
To enable from pytest: To enable from pytest:
```sh ```sh
pytest --log-cli-level info -k test_album_search_images pytest --log-cli-level debug -k test_album_search_images
``` ```
To also see data returned from the API, use `level=logging.DEBUG` or
`--log-cli-level debug` instead.

View file

@ -1,23 +1,42 @@
# Release Checklist # Release Checklist
* [ ] Get master to the appropriate code release state. [Travis CI](https://travis-ci.org/pylast/pylast) should be running cleanly for all merges to master.
- [ ] Get `main` to the appropriate code release state. * [ ] Remove `.dev0` suffix from the version and update version and date in the changelog:
[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:
https://github.com/pylast/pylast/releases
- [ ] Check next tag is correct, amend if needed
- [ ] Publish release
- [ ] 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:
```bash ```bash
pip3 uninstall -y pylast && pip3 install -U pylast && python3 -c "import pylast; print(pylast.__version__)" git checkout master
edit pylast/version.py
edit CHANGELOG.md
```
* [ ] Commit and tag with the version number:
```bash
git add CHANGELOG.md pylast/version.py
git commit -m "Release 3.0.0"
git tag -a 3.0.0 -m "Release 3.0.0"
```
* [ ] Create a distribution and release on PyPI:
```bash
pip3 install -U pip setuptools wheel twine keyring
rm -rf build
python3 setup.py sdist --format=gztar bdist_wheel
twine check dist/*
twine upload -r pypi dist/pylast-3.0.0*
```
* [ ] Check installation: `pip3 uninstall -y pylast && pip3 install -U pylast`
* [ ] Push commits and tags:
```bash
git push
git push --tags
```
* [ ] Create new GitHub release: https://github.com/pylast/pylast/releases/new
* Tag: Pick existing tag "3.0.0"
* Title: "Release 3.0.0"
* [ ] Increment version and append `.dev0`:
```bash
git checkout master
edit pylast/version.py
```
* [ ] Commit and push:
```bash
git add pylast/version.py
git commit -m "Start new release cycle"
git push
``` ```

View file

@ -1,4 +1,4 @@
username: TODO_ENTER_YOURS_HERE username: TODO_ENTER_YOURS_HERE
password_hash: TODO_ENTER_YOURS_HERE password_hash: TODO_ENTER_YOURS_HERE
api_key: TODO_ENTER_YOURS_HERE api_key: TODO_ENTER_YOURS_HERE
api_secret: TODO_ENTER_YOURS_HERE api_secret: TODO_ENTER_YOURS_HERE

File diff suppressed because it is too large Load diff

2
pylast/version.py Normal file
View file

@ -0,0 +1,2 @@
# Master version for pylast
__version__ = "3.1.0"

View file

@ -1,97 +0,0 @@
[build-system]
build-backend = "hatchling.build"
requires = [
"hatch-vcs",
"hatchling",
]
[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 <amr.hassan@gmail.com> and Contributors", email = "amr.hassan@gmail.com" },
]
requires-python = ">=3.8"
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: Apache Software License",
"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 :: 3.13",
"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",
]
optional-dependencies.tests = [
"flaky",
"pytest",
"pytest-cov",
"pytest-random-order",
"pyyaml",
]
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"
[tool.hatch.version.raw-options]
local_scheme = "no-local-version"
[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
"RUF100", # unused noqa (yesqa)
"UP", # pyupgrade
"W", # pycodestyle warnings
"YTT", # flake8-2020
]
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.pyproject-fmt]
max_supported_python = "3.13"

View file

@ -2,5 +2,3 @@
filterwarnings = filterwarnings =
once::DeprecationWarning once::DeprecationWarning
once::PendingDeprecationWarning once::PendingDeprecationWarning
xfail_strict=true

12
setup.cfg Normal file
View file

@ -0,0 +1,12 @@
[bdist_wheel]
universal = 1
[flake8]
ignore = W503
max_line_length = 88
[metadata]
license_file = COPYING
[pycodestyle]
max_line_length = 88

79
setup.py Executable file
View file

@ -0,0 +1,79 @@
#!/usr/bin/env python
import sys
from setuptools import find_packages, setup
version_dict = {}
with open("pylast/version.py") as f:
exec(f.read(), version_dict)
version = version_dict["__version__"]
if sys.version_info < (3, 5):
error = """pylast 3.0 and above are no longer compatible with Python 2.
This is pylast {} and you are using Python {}.
Make sure you have pip >= 9.0 and setuptools >= 24.2 and retry:
$ pip install --upgrade pip setuptools
Other choices:
- Upgrade to Python 3.
- Install an older version of pylast:
$ pip install 'pylast<3.0'
For more information:
https://github.com/pylast/pylast/issues/265
""".format(
version, ".".join([str(v) for v in sys.version_info[:3]])
)
print(error, file=sys.stderr)
sys.exit(1)
with open("README.md") as f:
long_description = f.read()
setup(
name="pylast",
description="A Python interface to Last.fm and Libre.fm",
long_description=long_description,
long_description_content_type="text/markdown",
version=version,
author="Amr Hassan <amr.hassan@gmail.com> and Contributors",
author_email="amr.hassan@gmail.com",
url="https://github.com/pylast/pylast",
tests_require=[
"coverage",
"flaky",
"mock",
"pycodestyle",
"pyflakes",
"pytest",
"pyyaml",
],
python_requires=">=3.5",
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.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
],
keywords=["Last.fm", "music", "scrobble", "scrobbling"],
packages=find_packages(exclude=("tests*",)),
license="Apache2",
)
# End of file

View file

@ -2,7 +2,8 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations import unittest
import warnings
import pylast import pylast
@ -10,7 +11,7 @@ from .test_pylast import TestPyLastWithLastFm
class TestPyLastAlbum(TestPyLastWithLastFm): class TestPyLastAlbum(TestPyLastWithLastFm):
def test_album_tags_are_topitems(self) -> None: def test_album_tags_are_topitems(self):
# Arrange # Arrange
album = self.network.get_album("Test Artist", "Test Album") album = self.network.get_album("Test Artist", "Test Album")
@ -18,28 +19,40 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
tags = album.get_top_tags(limit=1) tags = album.get_top_tags(limit=1)
# Assert # Assert
assert len(tags) > 0 self.assertGreater(len(tags), 0)
assert isinstance(tags[0], pylast.TopItem) self.assertIsInstance(tags[0], pylast.TopItem)
def test_album_is_hashable(self) -> None: def test_album_is_hashable(self):
# Arrange # Arrange
album = self.network.get_album("Test Artist", "Test Album") album = self.network.get_album("Test Artist", "Test Album")
# Act/Assert # Act/Assert
self.helper_is_thing_hashable(album) self.helper_is_thing_hashable(album)
def test_album_in_recent_tracks(self) -> None: def test_album_in_recent_tracks(self):
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
# Act # Act
# limit=2 to ignore now-playing: # limit=2 to ignore now-playing:
track = list(lastfm_user.get_recent_tracks(limit=2))[0] track = lastfm_user.get_recent_tracks(limit=2)[0]
# Assert # Assert
assert hasattr(track, "album") self.assertTrue(hasattr(track, "album"))
def test_album_wiki_content(self) -> None: def test_album_in_artist_tracks(self):
# Arrange
lastfm_user = self.network.get_user(self.username)
# Act
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
track = lastfm_user.get_artist_tracks(artist="Test Artist")[0]
# Assert
self.assertTrue(hasattr(track, "album"))
def test_album_wiki_content(self):
# Arrange # Arrange
album = pylast.Album("Test Artist", "Test Album", self.network) album = pylast.Album("Test Artist", "Test Album", self.network)
@ -47,10 +60,10 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
wiki = album.get_wiki_content() wiki = album.get_wiki_content()
# Assert # Assert
assert wiki is not None self.assertIsNotNone(wiki)
assert len(wiki) >= 1 self.assertGreaterEqual(len(wiki), 1)
def test_album_wiki_published_date(self) -> None: def test_album_wiki_published_date(self):
# Arrange # Arrange
album = pylast.Album("Test Artist", "Test Album", self.network) album = pylast.Album("Test Artist", "Test Album", self.network)
@ -58,10 +71,10 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
wiki = album.get_wiki_published_date() wiki = album.get_wiki_published_date()
# Assert # Assert
assert wiki is not None self.assertIsNotNone(wiki)
assert len(wiki) >= 1 self.assertGreaterEqual(len(wiki), 1)
def test_album_wiki_summary(self) -> None: def test_album_wiki_summary(self):
# Arrange # Arrange
album = pylast.Album("Test Artist", "Test Album", self.network) album = pylast.Album("Test Artist", "Test Album", self.network)
@ -69,26 +82,26 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
wiki = album.get_wiki_summary() wiki = album.get_wiki_summary()
# Assert # Assert
assert wiki is not None self.assertIsNotNone(wiki)
assert len(wiki) >= 1 self.assertGreaterEqual(len(wiki), 1)
def test_album_eq_none_is_false(self) -> None: def test_album_eq_none_is_false(self):
# Arrange # Arrange
album1 = None album1 = None
album2 = pylast.Album("Test Artist", "Test Album", self.network) album2 = pylast.Album("Test Artist", "Test Album", self.network)
# Act / Assert # Act / Assert
assert album1 != album2 self.assertNotEqual(album1, album2)
def test_album_ne_none_is_true(self) -> None: def test_album_ne_none_is_true(self):
# Arrange # Arrange
album1 = None album1 = None
album2 = pylast.Album("Test Artist", "Test Album", self.network) album2 = pylast.Album("Test Artist", "Test Album", self.network)
# Act / Assert # Act / Assert
assert album1 != album2 self.assertNotEqual(album1, album2)
def test_get_cover_image(self) -> None: def test_get_cover_image(self):
# Arrange # Arrange
album = self.network.get_album("Test Artist", "Test Album") album = self.network.get_album("Test Artist", "Test Album")
@ -96,25 +109,9 @@ class TestPyLastAlbum(TestPyLastWithLastFm):
image = album.get_cover_image() image = album.get_cover_image()
# Assert # Assert
assert image.startswith("https://") self.assert_startswith(image, "https://")
assert image.endswith(".gif") or image.endswith(".png") self.assert_endswith(image, ".png")
def test_mbid(self) -> None:
# Arrange
album = self.network.get_album("Radiohead", "OK Computer")
# Act if __name__ == "__main__":
mbid = album.get_mbid() unittest.main(failfast=True)
# Assert
assert mbid == "0b6b4ba0-d36f-47bd-b4ea-6a5b91842d29"
def test_no_mbid(self) -> None:
# Arrange
album = self.network.get_album("Test Artist", "Test Album")
# Act
mbid = album.get_mbid()
# Assert
assert mbid is None

View file

@ -2,17 +2,15 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations import unittest
import pytest
import pylast import pylast
from .test_pylast import WRITE_TEST, TestPyLastWithLastFm from .test_pylast import TestPyLastWithLastFm
class TestPyLastArtist(TestPyLastWithLastFm): class TestPyLastArtist(TestPyLastWithLastFm):
def test_repr(self) -> None: def test_repr(self):
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
@ -20,18 +18,18 @@ class TestPyLastArtist(TestPyLastWithLastFm):
representation = repr(artist) representation = repr(artist)
# Assert # Assert
assert representation.startswith("pylast.Artist('Test Artist',") self.assertTrue(representation.startswith("pylast.Artist('Test Artist',"))
def test_artist_is_hashable(self) -> None: def test_artist_is_hashable(self):
# Arrange # Arrange
test_artist = self.network.get_artist("Radiohead") test_artist = self.network.get_artist("Test Artist")
artist = test_artist.get_similar(limit=2)[0].item artist = test_artist.get_similar(limit=2)[0].item
assert isinstance(artist, pylast.Artist) self.assertIsInstance(artist, pylast.Artist)
# Act/Assert # Act/Assert
self.helper_is_thing_hashable(artist) self.helper_is_thing_hashable(artist)
def test_bio_published_date(self) -> None: def test_bio_published_date(self):
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
@ -39,10 +37,10 @@ class TestPyLastArtist(TestPyLastWithLastFm):
bio = artist.get_bio_published_date() bio = artist.get_bio_published_date()
# Assert # Assert
assert bio is not None self.assertIsNotNone(bio)
assert len(bio) >= 1 self.assertGreaterEqual(len(bio), 1)
def test_bio_content(self) -> None: def test_bio_content(self):
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
@ -50,21 +48,10 @@ class TestPyLastArtist(TestPyLastWithLastFm):
bio = artist.get_bio_content(language="en") bio = artist.get_bio_content(language="en")
# Assert # Assert
assert bio is not None self.assertIsNotNone(bio)
assert len(bio) >= 1 self.assertGreaterEqual(len(bio), 1)
def test_bio_content_none(self) -> None: def test_bio_summary(self):
# Arrange
# An artist with no biography, with "<content/>" in the API XML
artist = pylast.Artist("Mr Sizef + Unquote", self.network)
# Act
bio = artist.get_bio_content()
# Assert
assert bio is None
def test_bio_summary(self) -> None:
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
@ -72,10 +59,10 @@ class TestPyLastArtist(TestPyLastWithLastFm):
bio = artist.get_bio_summary(language="en") bio = artist.get_bio_summary(language="en")
# Assert # Assert
assert bio is not None self.assertIsNotNone(bio)
assert len(bio) >= 1 self.assertGreaterEqual(len(bio), 1)
def test_artist_top_tracks(self) -> None: def test_artist_top_tracks(self):
# Arrange # Arrange
# Pick an artist with plenty of plays # Pick an artist with plenty of plays
artist = self.network.get_top_artists(limit=1)[0].item artist = self.network.get_top_artists(limit=1)[0].item
@ -86,41 +73,54 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
self.helper_two_different_things_in_top_list(things, pylast.Track) self.helper_two_different_things_in_top_list(things, pylast.Track)
def test_artist_top_albums(self) -> None: def test_artist_top_albums(self):
# Arrange # Arrange
# Pick an artist with plenty of plays # Pick an artist with plenty of plays
artist = self.network.get_top_artists(limit=1)[0].item artist = self.network.get_top_artists(limit=1)[0].item
# Act # Act
things = list(artist.get_top_albums(limit=2)) things = artist.get_top_albums(limit=2)
# Assert # Assert
self.helper_two_different_things_in_top_list(things, pylast.Album) self.helper_two_different_things_in_top_list(things, pylast.Album)
@pytest.mark.parametrize("test_limit", [1, 50, 100]) def test_artist_top_albums_limit_1(self):
def test_artist_top_albums_limit(self, test_limit: int) -> None:
# Arrange # Arrange
limit = 1
# Pick an artist with plenty of plays # Pick an artist with plenty of plays
artist = self.network.get_top_artists(limit=1)[0].item artist = self.network.get_top_artists(limit=1)[0].item
# Act # Act
things = artist.get_top_albums(limit=test_limit) things = artist.get_top_albums(limit=limit)
# Assert # Assert
assert len(things) == test_limit self.assertEqual(len(things), 1)
def test_artist_top_albums_limit_default(self) -> None: def test_artist_top_albums_limit_50(self):
# Arrange # Arrange
limit = 50
# Pick an artist with plenty of plays # Pick an artist with plenty of plays
artist = self.network.get_top_artists(limit=1)[0].item artist = self.network.get_top_artists(limit=1)[0].item
# Act # Act
things = artist.get_top_albums() things = artist.get_top_albums(limit=limit)
# Assert # Assert
assert len(things) == 50 self.assertEqual(len(things), 50)
def test_artist_listener_count(self) -> None: def test_artist_top_albums_limit_100(self):
# Arrange
limit = 100
# Pick an artist with plenty of plays
artist = self.network.get_top_artists(limit=1)[0].item
# Act
things = artist.get_top_albums(limit=limit)
# Assert
self.assertEqual(len(things), 100)
def test_artist_listener_count(self):
# Arrange # Arrange
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
@ -128,11 +128,10 @@ class TestPyLastArtist(TestPyLastWithLastFm):
count = artist.get_listener_count() count = artist.get_listener_count()
# Assert # Assert
assert isinstance(count, int) self.assertIsInstance(count, int)
assert count > 0 self.assertGreater(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 # Arrange
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
# artist.clear_tags() # artist.clear_tags()
@ -142,12 +141,15 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags = artist.get_tags() tags = artist.get_tags()
assert len(tags) > 0 self.assertGreater(len(tags), 0)
found = any(tag.name == "testing" for tag in tags) found = False
assert found for tag in tags:
if tag.name == "testing":
found = True
break
self.assertTrue(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 # Arrange
tag = "testing" # text tag = "testing" # text
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
@ -158,11 +160,14 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags = artist.get_tags() tags = artist.get_tags()
found = any(tag.name == "testing" for tag in tags) found = False
assert not found for tag in tags:
if tag.name == "testing":
found = True
break
self.assertFalse(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 # Arrange
tag = pylast.Tag("testing", self.network) # Tag tag = pylast.Tag("testing", self.network) # Tag
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
@ -173,11 +178,14 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags = artist.get_tags() tags = artist.get_tags()
found = any(tag.name == "testing" for tag in tags) found = False
assert not found for tag in tags:
if tag.name == "testing":
found = True
break
self.assertFalse(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 # Arrange
tags = ["removetag1", "removetag2"] tags = ["removetag1", "removetag2"]
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
@ -190,14 +198,17 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags_after = artist.get_tags() tags_after = artist.get_tags()
assert len(tags_after) == len(tags_before) - 2 self.assertEqual(len(tags_after), len(tags_before) - 2)
found1 = any(tag.name == "removetag1" for tag in tags_after) found1, found2 = False, False
found2 = any(tag.name == "removetag2" for tag in tags_after) for tag in tags_after:
assert not found1 if tag.name == "removetag1":
assert not found2 found1 = True
elif tag.name == "removetag2":
found2 = True
self.assertFalse(found1)
self.assertFalse(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 # Arrange
tags = ["sometag1", "sometag2"] tags = ["sometag1", "sometag2"]
artist = self.network.get_artist("Test Artist 2") artist = self.network.get_artist("Test Artist 2")
@ -210,18 +221,18 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Assert # Assert
tags_after = artist.get_tags() tags_after = artist.get_tags()
assert tags_before != tags_after self.assertNotEqual(tags_before, tags_after)
assert len(tags_after) == 2 self.assertEqual(len(tags_after), 2)
found1, found2 = False, False found1, found2 = False, False
for tag in tags_after: for tag in tags_after:
if tag.name == "settag1": if tag.name == "settag1":
found1 = True found1 = True
elif tag.name == "settag2": elif tag.name == "settag2":
found2 = True found2 = True
assert found1 self.assertTrue(found1)
assert found2 self.assertTrue(found2)
def test_artists(self) -> None: def test_artists(self):
# Arrange # Arrange
artist1 = self.network.get_artist("Radiohead") artist1 = self.network.get_artist("Radiohead")
artist2 = self.network.get_artist("Portishead") artist2 = self.network.get_artist("Portishead")
@ -229,35 +240,38 @@ class TestPyLastArtist(TestPyLastWithLastFm):
# Act # Act
url = artist1.get_url() url = artist1.get_url()
mbid = artist1.get_mbid() mbid = artist1.get_mbid()
image = artist1.get_cover_image()
playcount = artist1.get_playcount() playcount = artist1.get_playcount()
streamable = artist1.is_streamable()
name = artist1.get_name(properly_capitalized=False) name = artist1.get_name(properly_capitalized=False)
name_cap = artist1.get_name(properly_capitalized=True) name_cap = artist1.get_name(properly_capitalized=True)
# Assert # Assert
assert playcount > 1 self.assertIn("https", image)
assert artist1 != artist2 self.assertGreater(playcount, 1)
assert name.lower() == name_cap.lower() self.assertNotEqual(artist1, artist2)
assert url == "https://www.last.fm/music/radiohead" self.assertEqual(name.lower(), name_cap.lower())
assert mbid == "a74b1b7f-71a5-4011-9441-d0b5e4122711" self.assertEqual(url, "https://www.last.fm/music/radiohead")
self.assertEqual(mbid, "a74b1b7f-71a5-4011-9441-d0b5e4122711")
self.assertIsInstance(streamable, bool)
def test_artist_eq_none_is_false(self) -> None: def test_artist_eq_none_is_false(self):
# Arrange # Arrange
artist1 = None artist1 = None
artist2 = pylast.Artist("Test Artist", self.network) artist2 = pylast.Artist("Test Artist", self.network)
# Act / Assert # Act / Assert
assert artist1 != artist2 self.assertNotEqual(artist1, artist2)
def test_artist_ne_none_is_true(self) -> None: def test_artist_ne_none_is_true(self):
# Arrange # Arrange
artist1 = None artist1 = None
artist2 = pylast.Artist("Test Artist", self.network) artist2 = pylast.Artist("Test Artist", self.network)
# Act / Assert # Act / Assert
assert artist1 != artist2 self.assertNotEqual(artist1, artist2)
def test_artist_get_correction(self) -> None: def test_artist_get_correction(self):
# Arrange # Arrange
artist = pylast.Artist("guns and roses", self.network) artist = pylast.Artist("guns and roses", self.network)
@ -265,9 +279,9 @@ class TestPyLastArtist(TestPyLastWithLastFm):
corrected_artist_name = artist.get_correction() corrected_artist_name = artist.get_correction()
# Assert # Assert
assert corrected_artist_name == "Guns N' Roses" self.assertEqual(corrected_artist_name, "Guns N' Roses")
def test_get_userplaycount(self) -> None: def test_get_userplaycount(self):
# Arrange # Arrange
artist = pylast.Artist("John Lennon", self.network, username=self.username) artist = pylast.Artist("John Lennon", self.network, username=self.username)
@ -275,4 +289,8 @@ class TestPyLastArtist(TestPyLastWithLastFm):
playcount = artist.get_userplaycount() playcount = artist.get_userplaycount()
# Assert # Assert
assert playcount >= 0 self.assertGreaterEqual(playcount, 0)
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -2,7 +2,7 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations import unittest
import pylast import pylast
@ -10,14 +10,14 @@ from .test_pylast import TestPyLastWithLastFm
class TestPyLastCountry(TestPyLastWithLastFm): class TestPyLastCountry(TestPyLastWithLastFm):
def test_country_is_hashable(self) -> None: def test_country_is_hashable(self):
# Arrange # Arrange
country = self.network.get_country("Italy") country = self.network.get_country("Italy")
# Act/Assert # Act/Assert
self.helper_is_thing_hashable(country) self.helper_is_thing_hashable(country)
def test_countries(self) -> None: def test_countries(self):
# Arrange # Arrange
country1 = pylast.Country("Italy", self.network) country1 = pylast.Country("Italy", self.network)
country2 = pylast.Country("Finland", self.network) country2 = pylast.Country("Finland", self.network)
@ -28,9 +28,13 @@ class TestPyLastCountry(TestPyLastWithLastFm):
url = country1.get_url() url = country1.get_url()
# Assert # Assert
assert "Italy" in rep self.assertIn("Italy", rep)
assert "pylast.Country" in rep self.assertIn("pylast.Country", rep)
assert text == "Italy" self.assertEqual(text, "Italy")
assert country1 == country1 self.assertEqual(country1, country1)
assert country1 != country2 self.assertNotEqual(country1, country2)
assert url == "https://www.last.fm/place/italy" self.assertEqual(url, "https://www.last.fm/place/italy")
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -2,7 +2,7 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations import unittest
import pylast import pylast
@ -10,7 +10,7 @@ from .test_pylast import TestPyLastWithLastFm
class TestPyLastLibrary(TestPyLastWithLastFm): class TestPyLastLibrary(TestPyLastWithLastFm):
def test_repr(self) -> None: def test_repr(self):
# Arrange # Arrange
library = pylast.Library(user=self.username, network=self.network) library = pylast.Library(user=self.username, network=self.network)
@ -18,9 +18,9 @@ class TestPyLastLibrary(TestPyLastWithLastFm):
representation = repr(library) representation = repr(library)
# Assert # Assert
assert representation.startswith("pylast.Library(") self.assert_startswith(representation, "pylast.Library(")
def test_str(self) -> None: def test_str(self):
# Arrange # Arrange
library = pylast.Library(user=self.username, network=self.network) library = pylast.Library(user=self.username, network=self.network)
@ -28,23 +28,23 @@ class TestPyLastLibrary(TestPyLastWithLastFm):
string = str(library) string = str(library)
# Assert # Assert
assert string.endswith("'s Library") self.assert_endswith(string, "'s Library")
def test_library_is_hashable(self) -> None: def test_library_is_hashable(self):
# Arrange # Arrange
library = pylast.Library(user=self.username, network=self.network) library = pylast.Library(user=self.username, network=self.network)
# Act/Assert # Act/Assert
self.helper_is_thing_hashable(library) self.helper_is_thing_hashable(library)
def test_cacheable_library(self) -> None: def test_cacheable_library(self):
# Arrange # Arrange
library = pylast.Library(self.username, self.network) library = pylast.Library(self.username, self.network)
# Act/Assert # Act/Assert
self.helper_validate_cacheable(library, "get_artists") self.helper_validate_cacheable(library, "get_artists")
def test_get_user(self) -> None: def test_get_user(self):
# Arrange # Arrange
library = pylast.Library(user=self.username, network=self.network) library = pylast.Library(user=self.username, network=self.network)
user_to_get = self.network.get_user(self.username) user_to_get = self.network.get_user(self.username)
@ -53,4 +53,8 @@ class TestPyLastLibrary(TestPyLastWithLastFm):
library_user = library.get_user() library_user = library.get_user()
# Assert # Assert
assert library_user == user_to_get self.assertEqual(library_user, user_to_get)
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -2,20 +2,20 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations import unittest
from flaky import flaky from flaky import flaky
import pylast import pylast
from .test_pylast import load_secrets from .test_pylast import PyLastTestCase, load_secrets
@flaky(max_runs=3, min_passes=1) @flaky(max_runs=3, min_passes=1)
class TestPyLastWithLibreFm: class TestPyLastWithLibreFm(PyLastTestCase):
"""Own class for Libre.fm because we don't need the Last.fm setUp""" """Own class for Libre.fm because we don't need the Last.fm setUp"""
def test_libre_fm(self) -> None: def test_libre_fm(self):
# Arrange # Arrange
secrets = load_secrets() secrets = load_secrets()
username = secrets["username"] username = secrets["username"]
@ -27,9 +27,9 @@ class TestPyLastWithLibreFm:
name = artist.get_name() name = artist.get_name()
# Assert # Assert
assert name == "Radiohead" self.assertEqual(name, "Radiohead")
def test_repr(self) -> None: def test_repr(self):
# Arrange # Arrange
secrets = load_secrets() secrets = load_secrets()
username = secrets["username"] username = secrets["username"]
@ -40,4 +40,8 @@ class TestPyLastWithLibreFm:
representation = repr(network) representation = repr(network)
# Assert # Assert
assert representation.startswith("pylast.LibreFMNetwork(") self.assert_startswith(representation, "pylast.LibreFMNetwork(")
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -1,22 +1,18 @@
#!/usr/bin/env python
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations
import re
import time import time
import unittest
import pytest
import pylast import pylast
from .test_pylast import WRITE_TEST, TestPyLastWithLastFm from .test_pylast import PY37, TestPyLastWithLastFm
class TestPyLastNetwork(TestPyLastWithLastFm): class TestPyLastNetwork(TestPyLastWithLastFm):
@pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") @unittest.skipUnless(PY37, "Only run on Python 3.7 to avoid collisions")
def test_scrobble(self) -> None: def test_scrobble(self):
# Arrange # Arrange
artist = "test artist" artist = "test artist"
title = "test title" title = "test title"
@ -29,12 +25,11 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
# limit=2 to ignore now-playing: # limit=2 to ignore now-playing:
last_scrobble = list(lastfm_user.get_recent_tracks(limit=2))[0] last_scrobble = lastfm_user.get_recent_tracks(limit=2)[0]
assert str(last_scrobble.track.artist).lower() == artist self.assertEqual(str(last_scrobble.track.artist).lower(), artist)
assert str(last_scrobble.track.title).lower() == title self.assertEqual(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 # Arrange
artist = "Test Artist" artist = "Test Artist"
title = "test title" title = "test title"
@ -49,36 +44,31 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
current_track = lastfm_user.get_now_playing() current_track = lastfm_user.get_now_playing()
assert current_track is not None self.assertIsNotNone(current_track)
assert str(current_track.title).lower() == "test title" self.assertEqual(str(current_track.title).lower(), "test title")
assert str(current_track.artist).lower() == "test artist" self.assertEqual(str(current_track.artist).lower(), "test artist")
assert current_track.info["album"] == "Test Album"
assert current_track.get_album().title == "Test Album"
assert len(current_track.info["image"]) def test_enable_rate_limiting(self):
assert re.search(r"^http.+$", current_track.info["image"][pylast.SIZE_LARGE])
def test_enable_rate_limiting(self) -> None:
# Arrange # Arrange
assert not self.network.is_rate_limited() self.assertFalse(self.network.is_rate_limited())
# Act # Act
self.network.enable_rate_limit() self.network.enable_rate_limit()
then = time.time() then = time.time()
# Make some network call, limit not applied first time # Make some network call, limit not applied first time
self.network.get_top_artists() self.network.get_user(self.username)
# Make a second network call, limiting should be applied # Make a second network call, limiting should be applied
self.network.get_top_artists() self.network.get_top_artists()
now = time.time() now = time.time()
# Assert # Assert
assert self.network.is_rate_limited() self.assertTrue(self.network.is_rate_limited())
assert now - then >= 0.2 self.assertGreaterEqual(now - then, 0.2)
def test_disable_rate_limiting(self) -> None: def test_disable_rate_limiting(self):
# Arrange # Arrange
self.network.enable_rate_limit() self.network.enable_rate_limit()
assert self.network.is_rate_limited() self.assertTrue(self.network.is_rate_limited())
# Act # Act
self.network.disable_rate_limit() self.network.disable_rate_limit()
@ -88,26 +78,26 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
self.network.get_top_artists() self.network.get_top_artists()
# Assert # Assert
assert not self.network.is_rate_limited() self.assertFalse(self.network.is_rate_limited())
def test_lastfm_network_name(self) -> None: def test_lastfm_network_name(self):
# Act # Act
name = str(self.network) name = str(self.network)
# Assert # Assert
assert name == "Last.fm Network" self.assertEqual(name, "Last.fm Network")
def test_geo_get_top_artists(self) -> None: def test_geo_get_top_artists(self):
# Arrange # Arrange
# Act # Act
artists = self.network.get_geo_top_artists(country="United Kingdom", limit=1) artists = self.network.get_geo_top_artists(country="United Kingdom", limit=1)
# Assert # Assert
assert len(artists) == 1 self.assertEqual(len(artists), 1)
assert isinstance(artists[0], pylast.TopItem) self.assertIsInstance(artists[0], pylast.TopItem)
assert isinstance(artists[0].item, pylast.Artist) self.assertIsInstance(artists[0].item, pylast.Artist)
def test_geo_get_top_tracks(self) -> None: def test_geo_get_top_tracks(self):
# Arrange # Arrange
# Act # Act
tracks = self.network.get_geo_top_tracks( tracks = self.network.get_geo_top_tracks(
@ -115,11 +105,11 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
) )
# Assert # Assert
assert len(tracks) == 1 self.assertEqual(len(tracks), 1)
assert isinstance(tracks[0], pylast.TopItem) self.assertIsInstance(tracks[0], pylast.TopItem)
assert isinstance(tracks[0].item, pylast.Track) self.assertIsInstance(tracks[0].item, pylast.Track)
def test_network_get_top_artists_with_limit(self) -> None: def test_network_get_top_artists_with_limit(self):
# Arrange # Arrange
# Act # Act
artists = self.network.get_top_artists(limit=1) artists = self.network.get_top_artists(limit=1)
@ -127,7 +117,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(artists, pylast.Artist) self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
def test_network_get_top_tags_with_limit(self) -> None: def test_network_get_top_tags_with_limit(self):
# Arrange # Arrange
# Act # Act
tags = self.network.get_top_tags(limit=1) tags = self.network.get_top_tags(limit=1)
@ -135,7 +125,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(tags, pylast.Tag) self.helper_only_one_thing_in_top_list(tags, pylast.Tag)
def test_network_get_top_tags_with_no_limit(self) -> None: def test_network_get_top_tags_with_no_limit(self):
# Arrange # Arrange
# Act # Act
tags = self.network.get_top_tags() tags = self.network.get_top_tags()
@ -143,7 +133,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
self.helper_at_least_one_thing_in_top_list(tags, pylast.Tag) self.helper_at_least_one_thing_in_top_list(tags, pylast.Tag)
def test_network_get_top_tracks_with_limit(self) -> None: def test_network_get_top_tracks_with_limit(self):
# Arrange # Arrange
# Act # Act
tracks = self.network.get_top_tracks(limit=1) tracks = self.network.get_top_tracks(limit=1)
@ -151,7 +141,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(tracks, pylast.Track) self.helper_only_one_thing_in_top_list(tracks, pylast.Track)
def test_country_top_tracks(self) -> None: def test_country_top_tracks(self):
# Arrange # Arrange
country = self.network.get_country("Croatia") country = self.network.get_country("Croatia")
@ -161,7 +151,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
self.helper_two_different_things_in_top_list(things, pylast.Track) self.helper_two_different_things_in_top_list(things, pylast.Track)
def test_country_network_top_tracks(self) -> None: def test_country_network_top_tracks(self):
# Arrange # Arrange
# Act # Act
things = self.network.get_geo_top_tracks("Croatia", limit=2) things = self.network.get_geo_top_tracks("Croatia", limit=2)
@ -169,7 +159,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
self.helper_two_different_things_in_top_list(things, pylast.Track) self.helper_two_different_things_in_top_list(things, pylast.Track)
def test_tag_top_tracks(self) -> None: def test_tag_top_tracks(self):
# Arrange # Arrange
tag = self.network.get_tag("blues") tag = self.network.get_tag("blues")
@ -179,7 +169,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
self.helper_two_different_things_in_top_list(things, pylast.Track) self.helper_two_different_things_in_top_list(things, pylast.Track)
def test_album_data(self) -> None: def test_album_data(self):
# Arrange # Arrange
thing = self.network.get_album("Test Artist", "Test Album") thing = self.network.get_album("Test Artist", "Test Album")
@ -192,14 +182,14 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
url = thing.get_url() url = thing.get_url()
# Assert # Assert
assert stringed == "Test Artist - Test Album" self.assertEqual(stringed, "Test Artist - Test Album")
assert "pylast.Album('Test Artist', 'Test Album'," in rep self.assertIn("pylast.Album('Test Artist', 'Test Album',", rep)
assert title == name self.assertEqual(title, name)
assert isinstance(playcount, int) self.assertIsInstance(playcount, int)
assert playcount > 1 self.assertGreater(playcount, 1)
assert "https://www.last.fm/music/test%2bartist/test%2balbum" == url self.assertEqual("https://www.last.fm/music/test%2bartist/test%2balbum", url)
def test_track_data(self) -> None: def test_track_data(self):
# Arrange # Arrange
thing = self.network.get_track("Test Artist", "test title") thing = self.network.get_track("Test Artist", "test title")
@ -212,15 +202,17 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
url = thing.get_url(pylast.DOMAIN_FRENCH) url = thing.get_url(pylast.DOMAIN_FRENCH)
# Assert # Assert
assert stringed == "Test Artist - test title" self.assertEqual(stringed, "Test Artist - test title")
assert "pylast.Track('Test Artist', 'test title'," in rep self.assertIn("pylast.Track('Test Artist', 'test title',", rep)
assert title == "test title" self.assertEqual(title, "test title")
assert title == name self.assertEqual(title, name)
assert isinstance(playcount, int) self.assertIsInstance(playcount, int)
assert playcount > 1 self.assertGreater(playcount, 1)
assert "https://www.last.fm/fr/music/test%2bartist/_/test%2btitle" == url self.assertEqual(
"https://www.last.fm/fr/music/test%2bartist/_/test%2btitle", url
)
def test_country_top_artists(self) -> None: def test_country_top_artists(self):
# Arrange # Arrange
country = self.network.get_country("Ukraine") country = self.network.get_country("Ukraine")
@ -230,7 +222,7 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(artists, pylast.Artist) self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
def test_caching(self) -> None: def test_caching(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -240,25 +232,25 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
tags2 = user.get_top_tags(limit=1, cacheable=True) tags2 = user.get_top_tags(limit=1, cacheable=True)
# Assert # Assert
assert self.network.is_caching_enabled() self.assertTrue(self.network.is_caching_enabled())
assert tags1 == tags2 self.assertEqual(tags1, tags2)
self.network.disable_caching() self.network.disable_caching()
assert not self.network.is_caching_enabled() self.assertFalse(self.network.is_caching_enabled())
def test_album_mbid(self) -> None: def test_album_mbid(self):
# Arrange # Arrange
mbid = "03c91c40-49a6-44a7-90e7-a700edf97a62" mbid = "a6a265bf-9f81-4055-8224-f7ac0aa6b937"
# Act # Act
album = self.network.get_album_by_mbid(mbid) album = self.network.get_album_by_mbid(mbid)
album_mbid = album.get_mbid() album_mbid = album.get_mbid()
# Assert # Assert
assert isinstance(album, pylast.Album) self.assertIsInstance(album, pylast.Album)
assert album.title == "Believe" self.assertEqual(album.title.lower(), "test")
assert album_mbid == mbid self.assertEqual(album_mbid, mbid)
def test_artist_mbid(self) -> None: def test_artist_mbid(self):
# Arrange # Arrange
mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55" mbid = "7e84f845-ac16-41fe-9ff8-df12eb32af55"
@ -266,10 +258,10 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
artist = self.network.get_artist_by_mbid(mbid) artist = self.network.get_artist_by_mbid(mbid)
# Assert # Assert
assert isinstance(artist, pylast.Artist) self.assertIsInstance(artist, pylast.Artist)
assert artist.name in ("MusicBrainz Test Artist", "MusicBrainzz Test Artist") self.assertEqual(artist.name, "MusicBrainz Test Artist")
def test_track_mbid(self) -> None: def test_track_mbid(self):
# Arrange # Arrange
mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0" mbid = "ebc037b1-cc9c-44f2-a21f-83c219f0e1e0"
@ -278,11 +270,11 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
track_mbid = track.get_mbid() track_mbid = track.get_mbid()
# Assert # Assert
assert isinstance(track, pylast.Track) self.assertIsInstance(track, pylast.Track)
assert track.title == "first" self.assertEqual(track.title, "first")
assert track_mbid == mbid self.assertEqual(track_mbid, mbid)
def test_init_with_token(self) -> None: def test_init_with_token(self):
# Arrange/Act # Arrange/Act
msg = None msg = None
try: try:
@ -295,21 +287,22 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
msg = str(exc) msg = str(exc)
# Assert # Assert
assert msg == "Unauthorized Token - This token has not been issued" self.assertEqual(msg, "Unauthorized Token - This token has not been issued")
def test_proxy(self) -> None: def test_proxy(self):
# Arrange # Arrange
proxy = "http://example.com:1234" host = "https://example.com"
port = 1234
# Act / Assert # Act / Assert
self.network.enable_proxy(proxy) self.network.enable_proxy(host, port)
assert self.network.is_proxy_enabled() self.assertTrue(self.network.is_proxy_enabled())
assert self.network.proxy == "http://example.com:1234" self.assertEqual(self.network._get_proxy(), ["https://example.com", 1234])
self.network.disable_proxy() self.network.disable_proxy()
assert not self.network.is_proxy_enabled() self.assertFalse(self.network.is_proxy_enabled())
def test_album_search(self) -> None: def test_album_search(self):
# Arrange # Arrange
album = "Nevermind" album = "Nevermind"
@ -318,10 +311,10 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
results = search.get_next_page() results = search.get_next_page()
# Assert # Assert
assert isinstance(results, list) self.assertIsInstance(results, list)
assert isinstance(results[0], pylast.Album) self.assertIsInstance(results[0], pylast.Album)
def test_album_search_images(self) -> None: def test_album_search_images(self):
# Arrange # Arrange
album = "Nevermind" album = "Nevermind"
search = self.network.search_for_album(album) search = self.network.search_for_album(album)
@ -331,17 +324,17 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
images = results[0].info["image"] images = results[0].info["image"]
# Assert # Assert
assert len(images) == 4 self.assertEqual(len(images), 4)
assert images[pylast.SIZE_SMALL].startswith("https://") self.assert_startswith(images[pylast.SIZE_SMALL], "https://")
assert images[pylast.SIZE_SMALL].endswith(".png") self.assert_endswith(images[pylast.SIZE_SMALL], ".png")
assert "/34s/" in images[pylast.SIZE_SMALL] self.assertIn("/34s/", images[pylast.SIZE_SMALL])
assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://") self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://")
assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png") self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png")
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] self.assertIn("/300x300/", images[pylast.SIZE_EXTRA_LARGE])
def test_artist_search(self) -> None: def test_artist_search(self):
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
@ -350,10 +343,10 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
results = search.get_next_page() results = search.get_next_page()
# Assert # Assert
assert isinstance(results, list) self.assertIsInstance(results, list)
assert isinstance(results[0], pylast.Artist) self.assertIsInstance(results[0], pylast.Artist)
def test_artist_search_images(self) -> None: def test_artist_search_images(self):
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
search = self.network.search_for_artist(artist) search = self.network.search_for_artist(artist)
@ -363,17 +356,17 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
images = results[0].info["image"] images = results[0].info["image"]
# Assert # Assert
assert len(images) == 5 self.assertEqual(len(images), 5)
assert images[pylast.SIZE_SMALL].startswith("https://") self.assert_startswith(images[pylast.SIZE_SMALL], "https://")
assert images[pylast.SIZE_SMALL].endswith(".png") self.assert_endswith(images[pylast.SIZE_SMALL], ".png")
assert "/34s/" in images[pylast.SIZE_SMALL] self.assertIn("/34s/", images[pylast.SIZE_SMALL])
assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://") self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://")
assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png") self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png")
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] self.assertIn("/300x300/", images[pylast.SIZE_EXTRA_LARGE])
def test_track_search(self) -> None: def test_track_search(self):
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
track = "Smells Like Teen Spirit" track = "Smells Like Teen Spirit"
@ -383,10 +376,10 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
results = search.get_next_page() results = search.get_next_page()
# Assert # Assert
assert isinstance(results, list) self.assertIsInstance(results, list)
assert isinstance(results[0], pylast.Track) self.assertIsInstance(results[0], pylast.Track)
def test_track_search_images(self) -> None: def test_track_search_images(self):
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
track = "Smells Like Teen Spirit" track = "Smells Like Teen Spirit"
@ -397,17 +390,17 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
images = results[0].info["image"] images = results[0].info["image"]
# Assert # Assert
assert len(images) == 4 self.assertEqual(len(images), 4)
assert images[pylast.SIZE_SMALL].startswith("https://") self.assert_startswith(images[pylast.SIZE_SMALL], "https://")
assert images[pylast.SIZE_SMALL].endswith(".png") self.assert_endswith(images[pylast.SIZE_SMALL], ".png")
assert "/34s/" in images[pylast.SIZE_SMALL] self.assertIn("/34s/", images[pylast.SIZE_SMALL])
assert images[pylast.SIZE_EXTRA_LARGE].startswith("https://") self.assert_startswith(images[pylast.SIZE_EXTRA_LARGE], "https://")
assert images[pylast.SIZE_EXTRA_LARGE].endswith(".png") self.assert_endswith(images[pylast.SIZE_EXTRA_LARGE], ".png")
assert "/300x300/" in images[pylast.SIZE_EXTRA_LARGE] self.assertIn("/300x300/", images[pylast.SIZE_EXTRA_LARGE])
def test_search_get_total_result_count(self) -> None: def test_search_get_total_result_count(self):
# Arrange # Arrange
artist = "Nirvana" artist = "Nirvana"
track = "Smells Like Teen Spirit" track = "Smells Like Teen Spirit"
@ -417,4 +410,8 @@ class TestPyLastNetwork(TestPyLastWithLastFm):
total = search.get_total_result_count() total = search.get_total_result_count()
# Assert # Assert
assert int(total) > 10000 self.assertGreater(int(total), 10000)
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -2,25 +2,26 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations
import os import os
import sys
import time import time
import unittest
import pytest import pytest
from flaky import flaky from flaky import flaky
import pylast import pylast
WRITE_TEST = False
PY37 = sys.version_info[:2] == (3, 7)
def load_secrets(): # pragma: no cover def load_secrets():
secrets_file = "test_pylast.yaml" secrets_file = "test_pylast.yaml"
if os.path.isfile(secrets_file): if os.path.isfile(secrets_file):
import yaml # pip install pyyaml import yaml # pip install pyyaml
with open(secrets_file) as f: # see example_test_pylast.yaml with open(secrets_file, "r") as f: # see example_test_pylast.yaml
doc = yaml.load(f) doc = yaml.load(f)
else: else:
doc = {} doc = {}
@ -34,39 +35,40 @@ def load_secrets(): # pragma: no cover
return doc return doc
def _no_xfail_rerun_filter(err, name, test, plugin) -> bool: class PyLastTestCase(unittest.TestCase):
for _ in test.iter_markers(name="xfail"): def assert_startswith(self, str, prefix, start=None, end=None):
return False self.assertTrue(str.startswith(prefix, start, end))
def assert_endswith(self, str, suffix, start=None, end=None):
self.assertTrue(str.endswith(suffix, start, end))
@flaky(max_runs=3, min_passes=1, rerun_filter=_no_xfail_rerun_filter) @flaky(max_runs=3, min_passes=1)
class TestPyLastWithLastFm: class TestPyLastWithLastFm(PyLastTestCase):
secrets = None secrets = None
@staticmethod def unix_timestamp(self):
def unix_timestamp() -> int:
return int(time.time()) return int(time.time())
@classmethod def setUp(self):
def setup_class(cls) -> None: if self.__class__.secrets is None:
if cls.secrets is None: self.__class__.secrets = load_secrets()
cls.secrets = load_secrets()
cls.username = cls.secrets["username"] self.username = self.__class__.secrets["username"]
password_hash = cls.secrets["password_hash"] password_hash = self.__class__.secrets["password_hash"]
api_key = cls.secrets["api_key"] api_key = self.__class__.secrets["api_key"]
api_secret = cls.secrets["api_secret"] api_secret = self.__class__.secrets["api_secret"]
cls.network = pylast.LastFMNetwork( self.network = pylast.LastFMNetwork(
api_key=api_key, api_key=api_key,
api_secret=api_secret, api_secret=api_secret,
username=cls.username, username=self.username,
password_hash=password_hash, password_hash=password_hash,
) )
@staticmethod def helper_is_thing_hashable(self, thing):
def helper_is_thing_hashable(thing) -> None:
# Arrange # Arrange
things = set() things = set()
@ -74,22 +76,21 @@ class TestPyLastWithLastFm:
things.add(thing) things.add(thing)
# Assert # Assert
assert thing is not None self.assertIsNotNone(thing)
assert len(things) == 1 self.assertEqual(len(things), 1)
@staticmethod def helper_validate_results(self, a, b, c):
def helper_validate_results(a, b, c) -> None:
# Assert # Assert
assert a is not None self.assertIsNotNone(a)
assert b is not None self.assertIsNotNone(b)
assert c is not None self.assertIsNotNone(c)
assert isinstance(len(a), int) self.assertGreaterEqual(len(a), 0)
assert isinstance(len(b), int) self.assertGreaterEqual(len(b), 0)
assert isinstance(len(c), int) self.assertGreaterEqual(len(c), 0)
assert a == b self.assertEqual(a, b)
assert b == c self.assertEqual(b, c)
def helper_validate_cacheable(self, thing, function_name) -> None: def helper_validate_cacheable(self, thing, function_name):
# Arrange # Arrange
# get thing.function_name() # get thing.function_name()
func = getattr(thing, function_name, None) func = getattr(thing, function_name, None)
@ -97,42 +98,42 @@ class TestPyLastWithLastFm:
# Act # Act
result1 = func(limit=1, cacheable=False) result1 = func(limit=1, cacheable=False)
result2 = func(limit=1, cacheable=True) result2 = func(limit=1, cacheable=True)
result3 = list(func(limit=1)) result3 = func(limit=1)
# Assert # Assert
self.helper_validate_results(result1, result2, result3) self.helper_validate_results(result1, result2, result3)
@staticmethod def helper_at_least_one_thing_in_top_list(self, things, expected_type):
def helper_at_least_one_thing_in_top_list(things, expected_type) -> None:
# Assert # Assert
assert len(things) > 1 self.assertGreater(len(things), 1)
assert isinstance(things, list) self.assertIsInstance(things, list)
assert isinstance(things[0], pylast.TopItem) self.assertIsInstance(things[0], pylast.TopItem)
assert isinstance(things[0].item, expected_type) self.assertIsInstance(things[0].item, expected_type)
@staticmethod def helper_only_one_thing_in_top_list(self, things, expected_type):
def helper_only_one_thing_in_top_list(things, expected_type) -> None:
# Assert # Assert
assert len(things) == 1 self.assertEqual(len(things), 1)
assert isinstance(things, list) self.assertIsInstance(things, list)
assert isinstance(things[0], pylast.TopItem) self.assertIsInstance(things[0], pylast.TopItem)
assert isinstance(things[0].item, expected_type) self.assertIsInstance(things[0].item, expected_type)
@staticmethod def helper_only_one_thing_in_list(self, things, expected_type):
def helper_only_one_thing_in_list(things, expected_type) -> None:
# Assert # Assert
assert len(things) == 1 self.assertEqual(len(things), 1)
assert isinstance(things, list) self.assertIsInstance(things, list)
assert isinstance(things[0], expected_type) self.assertIsInstance(things[0], expected_type)
@staticmethod def helper_two_different_things_in_top_list(self, things, expected_type):
def helper_two_different_things_in_top_list(things, expected_type) -> None:
# Assert # Assert
assert len(things) == 2 self.assertEqual(len(things), 2)
thing1 = things[0] thing1 = things[0]
thing2 = things[1] thing2 = things[1]
assert isinstance(thing1, pylast.TopItem) self.assertIsInstance(thing1, pylast.TopItem)
assert isinstance(thing2, pylast.TopItem) self.assertIsInstance(thing2, pylast.TopItem)
assert isinstance(thing1.item, expected_type) self.assertIsInstance(thing1.item, expected_type)
assert isinstance(thing2.item, expected_type) self.assertIsInstance(thing2.item, expected_type)
assert thing1 != thing2 self.assertNotEqual(thing1, thing2)
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -2,7 +2,7 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations import unittest
import pylast import pylast
@ -10,14 +10,14 @@ from .test_pylast import TestPyLastWithLastFm
class TestPyLastTag(TestPyLastWithLastFm): class TestPyLastTag(TestPyLastWithLastFm):
def test_tag_is_hashable(self) -> None: def test_tag_is_hashable(self):
# Arrange # Arrange
tag = self.network.get_top_tags(limit=1)[0] tag = self.network.get_top_tags(limit=1)[0]
# Act/Assert # Act/Assert
self.helper_is_thing_hashable(tag) self.helper_is_thing_hashable(tag)
def test_tag_top_artists(self) -> None: def test_tag_top_artists(self):
# Arrange # Arrange
tag = self.network.get_tag("blues") tag = self.network.get_tag("blues")
@ -27,7 +27,7 @@ class TestPyLastTag(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(artists, pylast.Artist) self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
def test_tag_top_albums(self) -> None: def test_tag_top_albums(self):
# Arrange # Arrange
tag = self.network.get_tag("blues") tag = self.network.get_tag("blues")
@ -37,7 +37,7 @@ class TestPyLastTag(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(albums, pylast.Album) self.helper_only_one_thing_in_top_list(albums, pylast.Album)
def test_tags(self) -> None: def test_tags(self):
# Arrange # Arrange
tag1 = self.network.get_tag("blues") tag1 = self.network.get_tag("blues")
tag2 = self.network.get_tag("rock") tag2 = self.network.get_tag("rock")
@ -49,10 +49,14 @@ class TestPyLastTag(TestPyLastWithLastFm):
url = tag1.get_url() url = tag1.get_url()
# Assert # Assert
assert "blues" == tag_str self.assertEqual("blues", tag_str)
assert "pylast.Tag" in tag_repr self.assertIn("pylast.Tag", tag_repr)
assert "blues" in tag_repr self.assertIn("blues", tag_repr)
assert "blues" == name self.assertEqual("blues", name)
assert tag1 == tag1 self.assertEqual(tag1, tag1)
assert tag1 != tag2 self.assertNotEqual(tag1, tag2)
assert url == "https://www.last.fm/tag/blues" self.assertEqual(url, "https://www.last.fm/tag/blues")
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -1,21 +1,17 @@
#!/usr/bin/env python
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations
import time import time
import unittest
import pytest
import pylast import pylast
from .test_pylast import WRITE_TEST, TestPyLastWithLastFm from .test_pylast import PY37, TestPyLastWithLastFm
class TestPyLastTrack(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 # Arrange
artist = "Test Artist" artist = "Test Artist"
title = "test title" title = "test title"
@ -26,12 +22,12 @@ class TestPyLastTrack(TestPyLastWithLastFm):
track.love() track.love()
# Assert # Assert
loved = list(lastfm_user.get_loved_tracks(limit=1)) loved = lastfm_user.get_loved_tracks(limit=1)
assert str(loved[0].track.artist).lower() == "test artist" self.assertEqual(str(loved[0].track.artist).lower(), "test artist")
assert str(loved[0].track.title).lower() == "test title" self.assertEqual(str(loved[0].track.title).lower(), "test title")
@pytest.mark.skipif(not WRITE_TEST, reason="Only test once to avoid collisions") @unittest.skipUnless(PY37, "Only run on Python 3.7 to avoid collisions")
def test_unlove(self) -> None: def test_unlove(self):
# Arrange # Arrange
artist = pylast.Artist("Test Artist", self.network) artist = pylast.Artist("Test Artist", self.network)
title = "test title" title = "test title"
@ -44,12 +40,12 @@ class TestPyLastTrack(TestPyLastWithLastFm):
time.sleep(1) # Delay, for Last.fm latency. TODO Can this be removed later? time.sleep(1) # Delay, for Last.fm latency. TODO Can this be removed later?
# Assert # Assert
loved = list(lastfm_user.get_loved_tracks(limit=1)) loved = lastfm_user.get_loved_tracks(limit=1)
if len(loved): # OK to be empty but if not: if len(loved): # OK to be empty but if not:
assert str(loved[0].track.artist) != "Test Artist" self.assertNotEqual(str(loved[0].track.artist), "Test Artist")
assert str(loved[0].track.title) != "test title" self.assertNotEqual(str(loved[0].track.title), "test title")
def test_user_play_count_in_track_info(self) -> None: def test_user_play_count_in_track_info(self):
# Arrange # Arrange
artist = "Test Artist" artist = "Test Artist"
title = "test title" title = "test title"
@ -61,9 +57,9 @@ class TestPyLastTrack(TestPyLastWithLastFm):
count = track.get_userplaycount() count = track.get_userplaycount()
# Assert # Assert
assert count >= 0 self.assertGreaterEqual(count, 0)
def test_user_loved_in_track_info(self) -> None: def test_user_loved_in_track_info(self):
# Arrange # Arrange
artist = "Test Artist" artist = "Test Artist"
title = "test title" title = "test title"
@ -75,20 +71,20 @@ class TestPyLastTrack(TestPyLastWithLastFm):
loved = track.get_userloved() loved = track.get_userloved()
# Assert # Assert
assert loved is not None self.assertIsNotNone(loved)
assert isinstance(loved, bool) self.assertIsInstance(loved, bool)
assert not isinstance(loved, str) self.assertNotIsInstance(loved, str)
def test_track_is_hashable(self) -> None: def test_track_is_hashable(self):
# Arrange # Arrange
artist = self.network.get_artist("Test Artist") artist = self.network.get_artist("Test Artist")
track = artist.get_top_tracks(stream=False)[0].item track = artist.get_top_tracks()[0].item
assert isinstance(track, pylast.Track) self.assertIsInstance(track, pylast.Track)
# Act/Assert # Act/Assert
self.helper_is_thing_hashable(track) self.helper_is_thing_hashable(track)
def test_track_wiki_content(self) -> None: def test_track_wiki_content(self):
# Arrange # Arrange
track = pylast.Track("Test Artist", "test title", self.network) track = pylast.Track("Test Artist", "test title", self.network)
@ -96,10 +92,10 @@ class TestPyLastTrack(TestPyLastWithLastFm):
wiki = track.get_wiki_content() wiki = track.get_wiki_content()
# Assert # Assert
assert wiki is not None self.assertIsNotNone(wiki)
assert len(wiki) >= 1 self.assertGreaterEqual(len(wiki), 1)
def test_track_wiki_summary(self) -> None: def test_track_wiki_summary(self):
# Arrange # Arrange
track = pylast.Track("Test Artist", "test title", self.network) track = pylast.Track("Test Artist", "test title", self.network)
@ -107,20 +103,40 @@ class TestPyLastTrack(TestPyLastWithLastFm):
wiki = track.get_wiki_summary() wiki = track.get_wiki_summary()
# Assert # Assert
assert wiki is not None self.assertIsNotNone(wiki)
assert len(wiki) >= 1 self.assertGreaterEqual(len(wiki), 1)
def test_track_get_duration(self) -> None: def test_track_get_duration(self):
# Arrange # Arrange
track = pylast.Track("Daft Punk", "Something About Us", self.network) track = pylast.Track("Nirvana", "Lithium", self.network)
# Act # Act
duration = track.get_duration() duration = track.get_duration()
# Assert # Assert
assert duration >= 100000 self.assertGreaterEqual(duration, 200000)
def test_track_get_album(self) -> None: def test_track_is_streamable(self):
# Arrange
track = pylast.Track("Nirvana", "Lithium", self.network)
# Act
streamable = track.is_streamable()
# Assert
self.assertFalse(streamable)
def test_track_is_fulltrack_available(self):
# Arrange
track = pylast.Track("Nirvana", "Lithium", self.network)
# Act
fulltrack_available = track.is_fulltrack_available()
# Assert
self.assertFalse(fulltrack_available)
def test_track_get_album(self):
# Arrange # Arrange
track = pylast.Track("Nirvana", "Lithium", self.network) track = pylast.Track("Nirvana", "Lithium", self.network)
@ -128,9 +144,9 @@ class TestPyLastTrack(TestPyLastWithLastFm):
album = track.get_album() album = track.get_album()
# Assert # Assert
assert str(album) == "Nirvana - Nevermind" self.assertEqual(str(album), "Nirvana - Nevermind")
def test_track_get_similar(self) -> None: def test_track_get_similar(self):
# Arrange # Arrange
track = pylast.Track("Cher", "Believe", self.network) track = pylast.Track("Cher", "Believe", self.network)
@ -138,29 +154,33 @@ class TestPyLastTrack(TestPyLastWithLastFm):
similar = track.get_similar() similar = track.get_similar()
# Assert # Assert
found = any(str(track.item) == "Cher - Strong Enough" for track in similar) found = False
assert found for track in similar:
if str(track.item) == "Madonna - Vogue":
found = True
break
self.assertTrue(found)
def test_track_get_similar_limits(self) -> None: def test_track_get_similar_limits(self):
# Arrange # Arrange
track = pylast.Track("Cher", "Believe", self.network) track = pylast.Track("Cher", "Believe", self.network)
# Act/Assert # Act/Assert
assert len(track.get_similar(limit=20)) == 20 self.assertEqual(len(track.get_similar(limit=20)), 20)
assert len(track.get_similar(limit=10)) <= 10 self.assertLessEqual(len(track.get_similar(limit=10)), 10)
assert len(track.get_similar(limit=None)) >= 23 self.assertGreaterEqual(len(track.get_similar(limit=None)), 23)
assert len(track.get_similar(limit=0)) >= 23 self.assertGreaterEqual(len(track.get_similar(limit=0)), 23)
def test_tracks_notequal(self) -> None: def test_tracks_notequal(self):
# Arrange # Arrange
track1 = pylast.Track("Test Artist", "test title", self.network) track1 = pylast.Track("Test Artist", "test title", self.network)
track2 = pylast.Track("Test Artist", "Test Track", self.network) track2 = pylast.Track("Test Artist", "Test Track", self.network)
# Act # Act
# Assert # Assert
assert track1 != track2 self.assertNotEqual(track1, track2)
def test_track_title_prop_caps(self) -> None: def test_track_title_prop_caps(self):
# Arrange # Arrange
track = pylast.Track("test artist", "test title", self.network) track = pylast.Track("test artist", "test title", self.network)
@ -168,9 +188,9 @@ class TestPyLastTrack(TestPyLastWithLastFm):
title = track.get_title(properly_capitalized=True) title = track.get_title(properly_capitalized=True)
# Assert # Assert
assert title == "Test Title" self.assertEqual(title, "Test Title")
def test_track_listener_count(self) -> None: def test_track_listener_count(self):
# Arrange # Arrange
track = pylast.Track("test artist", "test title", self.network) track = pylast.Track("test artist", "test title", self.network)
@ -178,9 +198,9 @@ class TestPyLastTrack(TestPyLastWithLastFm):
count = track.get_listener_count() count = track.get_listener_count()
# Assert # Assert
assert count > 21 self.assertGreater(count, 21)
def test_album_tracks(self) -> None: def test_album_tracks(self):
# Arrange # Arrange
album = pylast.Album("Test Artist", "Test", self.network) album = pylast.Album("Test Artist", "Test", self.network)
@ -189,28 +209,28 @@ class TestPyLastTrack(TestPyLastWithLastFm):
url = tracks[0].get_url() url = tracks[0].get_url()
# Assert # Assert
assert isinstance(tracks, list) self.assertIsInstance(tracks, list)
assert isinstance(tracks[0], pylast.Track) self.assertIsInstance(tracks[0], pylast.Track)
assert len(tracks) == 1 self.assertEqual(len(tracks), 1)
assert url.startswith("https://www.last.fm/music/test") self.assertTrue(url.startswith("https://www.last.fm/music/test"))
def test_track_eq_none_is_false(self) -> None: def test_track_eq_none_is_false(self):
# Arrange # Arrange
track1 = None track1 = None
track2 = pylast.Track("Test Artist", "test title", self.network) track2 = pylast.Track("Test Artist", "test title", self.network)
# Act / Assert # Act / Assert
assert track1 != track2 self.assertNotEqual(track1, track2)
def test_track_ne_none_is_true(self) -> None: def test_track_ne_none_is_true(self):
# Arrange # Arrange
track1 = None track1 = None
track2 = pylast.Track("Test Artist", "test title", self.network) track2 = pylast.Track("Test Artist", "test title", self.network)
# Act / Assert # Act / Assert
assert track1 != track2 self.assertNotEqual(track1, track2)
def test_track_get_correction(self) -> None: def test_track_get_correction(self):
# Arrange # Arrange
track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network) track = pylast.Track("Guns N' Roses", "mrbrownstone", self.network)
@ -218,9 +238,9 @@ class TestPyLastTrack(TestPyLastWithLastFm):
corrected_track_name = track.get_correction() corrected_track_name = track.get_correction()
# Assert # Assert
assert corrected_track_name == "Mr. Brownstone" self.assertEqual(corrected_track_name, "Mr. Brownstone")
def test_track_with_no_mbid(self) -> None: def test_track_with_no_mbid(self):
# Arrange # Arrange
track = pylast.Track("Static-X", "Set It Off", self.network) track = pylast.Track("Static-X", "Set It Off", self.network)
@ -228,4 +248,8 @@ class TestPyLastTrack(TestPyLastWithLastFm):
mbid = track.get_mbid() mbid = track.get_mbid()
# Assert # Assert
assert mbid is None self.assertIsNone(mbid)
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -2,15 +2,9 @@
""" """
Integration (not unit) tests for pylast.py Integration (not unit) tests for pylast.py
""" """
from __future__ import annotations
import calendar
import datetime as dt
import inspect
import os import os
import re import unittest
import warnings
import pytest
import pylast import pylast
@ -18,7 +12,7 @@ from .test_pylast import TestPyLastWithLastFm
class TestPyLastUser(TestPyLastWithLastFm): class TestPyLastUser(TestPyLastWithLastFm):
def test_repr(self) -> None: def test_repr(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -26,9 +20,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
representation = repr(user) representation = repr(user)
# Assert # Assert
assert representation.startswith("pylast.User('RJ',") self.assert_startswith(representation, "pylast.User('RJ',")
def test_str(self) -> None: def test_str(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -36,9 +30,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
string = str(user) string = str(user)
# Assert # Assert
assert string == "RJ" self.assertEqual(string, "RJ")
def test_equality(self) -> None: def test_equality(self):
# Arrange # Arrange
user_1a = self.network.get_user("RJ") user_1a = self.network.get_user("RJ")
user_1b = self.network.get_user("RJ") user_1b = self.network.get_user("RJ")
@ -46,11 +40,11 @@ class TestPyLastUser(TestPyLastWithLastFm):
not_a_user = self.network not_a_user = self.network
# Act / Assert # Act / Assert
assert user_1a == user_1b self.assertEqual(user_1a, user_1b)
assert user_1a != user_2 self.assertNotEqual(user_1a, user_2)
assert user_1a != not_a_user self.assertNotEqual(user_1a, not_a_user)
def test_get_name(self) -> None: def test_get_name(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -58,9 +52,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
name = user.get_name(properly_capitalized=True) name = user.get_name(properly_capitalized=True)
# Assert # Assert
assert name == "RJ" self.assertEqual(name, "RJ")
def test_get_user_registration(self) -> None: def test_get_user_registration(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -70,13 +64,13 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
if int(registered): if int(registered):
# Last.fm API broken? Used to be yyyy-mm-dd not Unix timestamp # Last.fm API broken? Used to be yyyy-mm-dd not Unix timestamp
assert registered == "1037793040" self.assertEqual(registered, "1037793040")
else: # pragma: no cover else:
# Old way # Old way
# Just check date because of timezones # Just check date because of timezones
assert "2002-11-20 " in registered self.assertIn("2002-11-20 ", registered)
def test_get_user_unixtime_registration(self) -> None: def test_get_user_unixtime_registration(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -85,9 +79,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
# Just check date because of timezones # Just check date because of timezones
assert unixtime_registered == 1037793040 self.assertEqual(unixtime_registered, 1037793040)
def test_get_countryless_user(self) -> None: def test_get_countryless_user(self):
# Arrange # Arrange
# Currently test_user has no country set: # Currently test_user has no country set:
lastfm_user = self.network.get_user("test_user") lastfm_user = self.network.get_user("test_user")
@ -96,9 +90,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
country = lastfm_user.get_country() country = lastfm_user.get_country()
# Assert # Assert
assert country is None self.assertIsNone(country)
def test_user_get_country(self) -> None: def test_user_get_country(self):
# Arrange # Arrange
lastfm_user = self.network.get_user("RJ") lastfm_user = self.network.get_user("RJ")
@ -106,9 +100,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
country = lastfm_user.get_country() country = lastfm_user.get_country()
# Assert # Assert
assert str(country) == "United Kingdom" self.assertEqual(str(country), "United Kingdom")
def test_user_equals_none(self) -> None: def test_user_equals_none(self):
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
@ -116,9 +110,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
value = lastfm_user is None value = lastfm_user is None
# Assert # Assert
assert not value self.assertFalse(value)
def test_user_not_equal_to_none(self) -> None: def test_user_not_equal_to_none(self):
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
@ -126,9 +120,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
value = lastfm_user is not None value = lastfm_user is not None
# Assert # Assert
assert value self.assertTrue(value)
def test_now_playing_user_with_no_scrobbles(self) -> None: def test_now_playing_user_with_no_scrobbles(self):
# Arrange # Arrange
# Currently test-account has no scrobbles: # Currently test-account has no scrobbles:
user = self.network.get_user("test-account") user = self.network.get_user("test-account")
@ -137,20 +131,20 @@ class TestPyLastUser(TestPyLastWithLastFm):
current_track = user.get_now_playing() current_track = user.get_now_playing()
# Assert # Assert
assert current_track is None self.assertIsNone(current_track)
def test_love_limits(self) -> None: def test_love_limits(self):
# Arrange # Arrange
# Currently test-account has at least 23 loved tracks: # Currently test-account has at least 23 loved tracks:
user = self.network.get_user("test-user") user = self.network.get_user("test-user")
# Act/Assert # Act/Assert
assert len(user.get_loved_tracks(limit=20)) == 20 self.assertEqual(len(user.get_loved_tracks(limit=20)), 20)
assert len(user.get_loved_tracks(limit=100)) <= 100 self.assertLessEqual(len(user.get_loved_tracks(limit=100)), 100)
assert len(user.get_loved_tracks(limit=None)) >= 23 self.assertGreaterEqual(len(user.get_loved_tracks(limit=None)), 23)
assert len(user.get_loved_tracks(limit=0)) >= 23 self.assertGreaterEqual(len(user.get_loved_tracks(limit=0)), 23)
def test_user_is_hashable(self) -> None: def test_user_is_hashable(self):
# Arrange # Arrange
user = self.network.get_user(self.username) user = self.network.get_user(self.username)
@ -171,7 +165,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# # Assert # # Assert
# self.assertGreaterEqual(len(tracks), 0) # self.assertGreaterEqual(len(tracks), 0)
def test_pickle(self) -> None: def test_pickle(self):
# Arrange # Arrange
import pickle import pickle
@ -186,24 +180,32 @@ class TestPyLastUser(TestPyLastWithLastFm):
os.remove(filename) os.remove(filename)
# Assert # Assert
assert lastfm_user == loaded_user self.assertEqual(lastfm_user, loaded_user)
@pytest.mark.xfail def test_cacheable_user_artist_tracks(self):
def test_cacheable_user(self) -> None: # Arrange
lastfm_user = self.network.get_authenticated_user()
# Act
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
result1 = lastfm_user.get_artist_tracks("Test Artist", cacheable=False)
result2 = lastfm_user.get_artist_tracks("Test Artist", cacheable=True)
result3 = lastfm_user.get_artist_tracks("Test Artist")
# Assert
self.helper_validate_results(result1, result2, result3)
def test_cacheable_user(self):
# Arrange # Arrange
lastfm_user = self.network.get_authenticated_user() lastfm_user = self.network.get_authenticated_user()
# Act/Assert # Act/Assert
self.helper_validate_cacheable(lastfm_user, "get_friends") self.helper_validate_cacheable(lastfm_user, "get_friends")
# no cover whilst xfail: self.helper_validate_cacheable(lastfm_user, "get_loved_tracks")
self.helper_validate_cacheable( # pragma: no cover self.helper_validate_cacheable(lastfm_user, "get_recent_tracks")
lastfm_user, "get_loved_tracks"
)
self.helper_validate_cacheable( # pragma: no cover
lastfm_user, "get_recent_tracks"
)
def test_user_get_top_tags_with_limit(self) -> None: def test_user_get_top_tags_with_limit(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -213,7 +215,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(tags, pylast.Tag) self.helper_only_one_thing_in_top_list(tags, pylast.Tag)
def test_user_top_tracks(self) -> None: def test_user_top_tracks(self):
# Arrange # Arrange
lastfm_user = self.network.get_user("RJ") lastfm_user = self.network.get_user("RJ")
@ -223,14 +225,14 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
self.helper_two_different_things_in_top_list(things, pylast.Track) self.helper_two_different_things_in_top_list(things, pylast.Track)
def helper_assert_chart(self, chart, expected_type) -> None: def helper_assert_chart(self, chart, expected_type):
# Assert # Assert
assert chart is not None self.assertIsNotNone(chart)
assert len(chart) > 0 self.assertGreater(len(chart), 0)
assert isinstance(chart[0], pylast.TopItem) self.assertIsInstance(chart[0], pylast.TopItem)
assert isinstance(chart[0].item, expected_type) self.assertIsInstance(chart[0].item, expected_type)
def helper_get_assert_charts(self, thing, date) -> None: def helper_get_assert_charts(self, thing, date):
# Arrange # Arrange
album_chart, track_chart = None, None album_chart, track_chart = None, None
(from_date, to_date) = date (from_date, to_date) = date
@ -247,14 +249,14 @@ class TestPyLastUser(TestPyLastWithLastFm):
self.helper_assert_chart(album_chart, pylast.Album) self.helper_assert_chart(album_chart, pylast.Album)
self.helper_assert_chart(track_chart, pylast.Track) self.helper_assert_chart(track_chart, pylast.Track)
def helper_dates_valid(self, dates) -> None: def helper_dates_valid(self, dates):
# Assert # Assert
assert len(dates) >= 1 self.assertGreaterEqual(len(dates), 1)
assert isinstance(dates[0], tuple) self.assertIsInstance(dates[0], tuple)
(start, end) = dates[0] (start, end) = dates[0]
assert start < end self.assertLess(start, end)
def test_user_charts(self) -> None: def test_user_charts(self):
# Arrange # Arrange
lastfm_user = self.network.get_user("RJ") lastfm_user = self.network.get_user("RJ")
dates = lastfm_user.get_weekly_chart_dates() dates = lastfm_user.get_weekly_chart_dates()
@ -263,7 +265,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Act/Assert # Act/Assert
self.helper_get_assert_charts(lastfm_user, dates[0]) self.helper_get_assert_charts(lastfm_user, dates[0])
def test_user_top_artists(self) -> None: def test_user_top_artists(self):
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
@ -273,7 +275,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(artists, pylast.Artist) self.helper_only_one_thing_in_top_list(artists, pylast.Artist)
def test_user_top_albums(self) -> None: def test_user_top_albums(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -283,11 +285,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_top_list(albums, pylast.Album) self.helper_only_one_thing_in_top_list(albums, pylast.Album)
top_album = albums[0].item def test_user_tagged_artists(self):
assert len(top_album.info["image"])
assert re.search(r"^http.+$", top_album.info["image"][pylast.SIZE_LARGE])
def test_user_tagged_artists(self) -> None:
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
tags = ["artisttagola"] tags = ["artisttagola"]
@ -300,7 +298,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_list(artists, pylast.Artist) self.helper_only_one_thing_in_list(artists, pylast.Artist)
def test_user_tagged_albums(self) -> None: def test_user_tagged_albums(self):
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
tags = ["albumtagola"] tags = ["albumtagola"]
@ -313,7 +311,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_list(albums, pylast.Album) self.helper_only_one_thing_in_list(albums, pylast.Album)
def test_user_tagged_tracks(self) -> None: def test_user_tagged_tracks(self):
# Arrange # Arrange
lastfm_user = self.network.get_user(self.username) lastfm_user = self.network.get_user(self.username)
tags = ["tracktagola"] tags = ["tracktagola"]
@ -326,7 +324,7 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Assert # Assert
self.helper_only_one_thing_in_list(tracks, pylast.Track) self.helper_only_one_thing_in_list(tracks, pylast.Track)
def test_user_subscriber(self) -> None: def test_user_subscriber(self):
# Arrange # Arrange
subscriber = self.network.get_user("RJ") subscriber = self.network.get_user("RJ")
non_subscriber = self.network.get_user("Test User") non_subscriber = self.network.get_user("Test User")
@ -336,10 +334,10 @@ class TestPyLastUser(TestPyLastWithLastFm):
non_subscriber_is_subscriber = non_subscriber.is_subscriber() non_subscriber_is_subscriber = non_subscriber.is_subscriber()
# Assert # Assert
assert subscriber_is_subscriber self.assertTrue(subscriber_is_subscriber)
assert not non_subscriber_is_subscriber self.assertFalse(non_subscriber_is_subscriber)
def test_user_get_image(self) -> None: def test_user_get_image(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -347,9 +345,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
url = user.get_image() url = user.get_image()
# Assert # Assert
assert url.startswith("https://") self.assert_startswith(url, "https://")
def test_user_get_library(self) -> None: def test_user_get_library(self):
# Arrange # Arrange
user = self.network.get_user(self.username) user = self.network.get_user(self.username)
@ -357,13 +355,17 @@ class TestPyLastUser(TestPyLastWithLastFm):
library = user.get_library() library = user.get_library()
# Assert # Assert
assert isinstance(library, pylast.Library) self.assertIsInstance(library, pylast.Library)
def test_get_recent_tracks_from_to(self) -> None: def test_get_recent_tracks_from_to(self):
# Arrange # Arrange
lastfm_user = self.network.get_user("RJ") lastfm_user = self.network.get_user("RJ")
start = dt.datetime(2011, 7, 21, 15, 10)
end = dt.datetime(2011, 7, 21, 15, 15) from datetime import datetime
start = datetime(2011, 7, 21, 15, 10)
end = datetime(2011, 7, 21, 15, 15)
import calendar
utc_start = calendar.timegm(start.utctimetuple()) utc_start = calendar.timegm(start.utctimetuple())
utc_end = calendar.timegm(end.utctimetuple()) utc_end = calendar.timegm(end.utctimetuple())
@ -372,47 +374,11 @@ class TestPyLastUser(TestPyLastWithLastFm):
tracks = lastfm_user.get_recent_tracks(time_from=utc_start, time_to=utc_end) tracks = lastfm_user.get_recent_tracks(time_from=utc_start, time_to=utc_end)
# Assert # Assert
assert len(tracks) == 1 self.assertEqual(len(tracks), 1)
assert str(tracks[0].track.artist) == "Johnny Cash" self.assertEqual(str(tracks[0].track.artist), "Johnny Cash")
assert str(tracks[0].track.title) == "Ring of Fire" self.assertEqual(str(tracks[0].track.title), "Ring of Fire")
def test_get_recent_tracks_limit_none(self) -> None: def test_get_playcount(self):
# Arrange
lastfm_user = self.network.get_user("bbc6music")
start = dt.datetime(2020, 2, 15, 15, 00)
end = dt.datetime(2020, 2, 15, 15, 40)
utc_start = calendar.timegm(start.utctimetuple())
utc_end = calendar.timegm(end.utctimetuple())
# Act
tracks = lastfm_user.get_recent_tracks(
time_from=utc_start, time_to=utc_end, limit=None
)
# Assert
assert len(tracks) == 11
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) -> None:
# Arrange
lastfm_user = self.network.get_user("bbc6music")
start = dt.datetime(2020, 2, 15, 15, 00)
end = dt.datetime(2020, 2, 15, 15, 40)
utc_start = calendar.timegm(start.utctimetuple())
utc_end = calendar.timegm(end.utctimetuple())
# Act
tracks = lastfm_user.get_recent_tracks(
time_from=utc_start, time_to=utc_end, limit=None, stream=True
)
# Assert
assert inspect.isgenerator(tracks)
def test_get_playcount(self) -> None:
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -420,9 +386,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
playcount = user.get_playcount() playcount = user.get_playcount()
# Assert # Assert
assert playcount >= 128387 self.assertGreaterEqual(playcount, 128387)
def test_get_image(self) -> None: def test_get_image(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -430,10 +396,10 @@ class TestPyLastUser(TestPyLastWithLastFm):
image = user.get_image() image = user.get_image()
# Assert # Assert
assert image.startswith("https://") self.assert_startswith(image, "https://")
assert image.endswith(".png") self.assert_endswith(image, ".png")
def test_get_url(self) -> None: def test_get_url(self):
# Arrange # Arrange
user = self.network.get_user("RJ") user = self.network.get_user("RJ")
@ -441,9 +407,9 @@ class TestPyLastUser(TestPyLastWithLastFm):
url = user.get_url() url = user.get_url()
# Assert # Assert
assert url == "https://www.last.fm/user/rj" self.assertEqual(url, "https://www.last.fm/user/rj")
def test_get_weekly_artist_charts(self) -> None: def test_get_weekly_artist_charts(self):
# Arrange # Arrange
user = self.network.get_user("bbc6music") user = self.network.get_user("bbc6music")
@ -452,10 +418,10 @@ class TestPyLastUser(TestPyLastWithLastFm):
artist, weight = charts[0] artist, weight = charts[0]
# Assert # Assert
assert artist is not None self.assertIsNotNone(artist)
assert isinstance(artist.network, pylast.LastFMNetwork) self.assertIsInstance(artist.network, pylast.LastFMNetwork)
def test_get_weekly_track_charts(self) -> None: def test_get_weekly_track_charts(self):
# Arrange # Arrange
user = self.network.get_user("bbc6music") user = self.network.get_user("bbc6music")
@ -464,10 +430,10 @@ class TestPyLastUser(TestPyLastWithLastFm):
track, weight = charts[0] track, weight = charts[0]
# Assert # Assert
assert track is not None self.assertIsNotNone(track)
assert isinstance(track.network, pylast.LastFMNetwork) self.assertIsInstance(track.network, pylast.LastFMNetwork)
def test_user_get_track_scrobbles(self) -> None: def test_user_get_track_scrobbles(self):
# Arrange # Arrange
artist = "France Gall" artist = "France Gall"
title = "Laisse Tomber Les Filles" title = "Laisse Tomber Les Filles"
@ -477,11 +443,11 @@ class TestPyLastUser(TestPyLastWithLastFm):
scrobbles = user.get_track_scrobbles(artist, title) scrobbles = user.get_track_scrobbles(artist, title)
# Assert # Assert
assert len(scrobbles) > 0 self.assertGreater(len(scrobbles), 0)
assert str(scrobbles[0].track.artist) == "France Gall" self.assertEqual(str(scrobbles[0].track.artist), "France Gall")
assert scrobbles[0].track.title == "Laisse Tomber Les Filles" self.assertEqual(scrobbles[0].track.title, "Laisse Tomber Les Filles")
def test_cacheable_user_get_track_scrobbles(self) -> None: def test_cacheable_user_get_track_scrobbles(self):
# Arrange # Arrange
artist = "France Gall" artist = "France Gall"
title = "Laisse Tomber Les Filles" title = "Laisse Tomber Les Filles"
@ -489,8 +455,12 @@ class TestPyLastUser(TestPyLastWithLastFm):
# Act # Act
result1 = user.get_track_scrobbles(artist, title, cacheable=False) result1 = user.get_track_scrobbles(artist, title, cacheable=False)
result2 = list(user.get_track_scrobbles(artist, title, cacheable=True)) result2 = user.get_track_scrobbles(artist, title, cacheable=True)
result3 = list(user.get_track_scrobbles(artist, title)) result3 = user.get_track_scrobbles(artist, title)
# Assert # Assert
self.helper_validate_results(result1, result2, result3) self.helper_validate_results(result1, result2, result3)
if __name__ == "__main__":
unittest.main(failfast=True)

View file

@ -1,7 +1,5 @@
from __future__ import annotations # -*- coding: utf-8 -*-
import mock
from unittest import mock
import pytest import pytest
import pylast import pylast
@ -20,51 +18,12 @@ def mock_network():
"fdasfdsafsaf not unicode", "fdasfdsafsaf not unicode",
], ],
) )
def test_get_cache_key(artist) -> None: def test_get_cache_key(artist):
request = pylast._Request(mock_network(), "some_method", params={"artist": artist}) request = pylast._Request(mock_network(), "some_method", params={"artist": artist})
request._get_cache_key() request._get_cache_key()
@pytest.mark.parametrize("obj", [pylast.Artist("B\xe9l", mock_network())]) @pytest.mark.parametrize("obj", [pylast.Artist("B\xe9l", mock_network())])
def test_cast_and_hash(obj) -> None: def test_cast_and_hash(obj):
assert isinstance(str(obj), str) assert type(str(obj)) is str
assert isinstance(hash(obj), int) assert isinstance(hash(obj), int)
@pytest.mark.parametrize(
"test_input, expected",
[
(
# Plain text
'<album mbid="">test album name</album>',
'<album mbid="">test album name</album>',
),
(
# Contains Unicode ENQ Enquiry control character
'<album mbid="">test album \u0005name</album>',
'<album mbid="">test album name</album>',
),
],
)
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
'<album mbid="">test album name</album>',
'<?xml version="1.0" ?><album mbid="">test album name</album>',
),
(
# Contains Unicode ENQ Enquiry control character
'<album mbid="">test album \u0005name</album>',
'<?xml version="1.0" ?><album mbid="">test album name</album>',
),
],
)
def test__parse_response(test_input: str, expected: str) -> None:
doc = pylast._parse_response(test_input)
assert doc.toxml() == expected

56
tox.ini
View file

@ -1,40 +1,32 @@
[tox] [tox]
requires = envlist = py37, py36, py35, pypy3, py38dev
tox>=4.2 recreate = False
env_list =
lint
py{py3, 313, 312, 311, 310, 39, 38}
[testenv] [testenv]
extras = setenv =
tests PYLAST_USERNAME={env:PYLAST_USERNAME:}
pass_env = PYLAST_PASSWORD_HASH={env:PYLAST_PASSWORD_HASH:}
FORCE_COLOR PYLAST_API_KEY={env:PYLAST_API_KEY:}
PYLAST_API_KEY PYLAST_API_SECRET={env:PYLAST_API_SECRET:}
PYLAST_API_SECRET
PYLAST_PASSWORD_HASH
PYLAST_USERNAME
commands =
{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
deps = deps =
pre-commit pyyaml
pass_env = pytest
PRE_COMMIT_COLOR mock
commands = ipdb
pre-commit run --all-files --show-diff-on-failure pytest-cov
pytest-random-order
flaky
commands = pytest -v -s -W all --cov pylast --cov-report term-missing --random-order {posargs}
[testenv:venv] [testenv:venv]
deps = ipdb
commands = {posargs}
[testenv:lint]
deps = deps =
ipdb flake8
pep8-naming
black
commands = commands =
{posargs} flake8 .
black --check --diff .