Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add mypy (non-strict) #498

Merged
merged 1 commit into from
Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Ruff
name: lint

on:
pull_request:
Expand All @@ -13,4 +13,13 @@ jobs:
- uses: chartboost/ruff-action@v1
- uses: chartboost/ruff-action@v1
with:
args: format --check
args: format --check
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
with:
python-version: "3.11"
- run: pip install mypy==1.8.0
- run: mypy --install-types --non-interactive
17 changes: 8 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
repos:
- repo: https://github.com/pre-commit/mirrors-yapf
rev: v0.32.0
hooks:
- id: yapf
additional_dependencies: [toml]
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.9
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
- id: ruff-format
4 changes: 3 additions & 1 deletion CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ Before making changes to the code, install the development requirements using

.. code-block::

pip install -e .[dev]
pip install pipx
pipx install pdm pre-commit
pdm install

Before committing, stage your files and run style and linter checks:

Expand Down
77 changes: 76 additions & 1 deletion pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,17 @@ extend-select = [
"I", # isort
]

[tool.mypy]
files = [
"ytmusicapi/"
]
mypy_path = "ytmusicapi"

[tool.pdm.dev-dependencies]
dev = [
"coverage>=7.4.0",
'sphinx<7',
'sphinx-rtd-theme',
"ruff>=0.1.9",
"mypy>=1.8.0",
]
1 change: 1 addition & 0 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ def test_get_search_suggestions(self):
# add search term to history
first_pass = self.yt_auth.search("b")
self.assertGreater(len(first_pass), 0)
time.sleep(3)
# get results
results = self.yt_auth.get_search_suggestions("b", detailed_runs=True)
self.assertGreater(len(results), 0)
Expand Down
3 changes: 2 additions & 1 deletion ytmusicapi/auth/browser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import platform
from typing import Optional

from requests.structures import CaseInsensitiveDict

Expand All @@ -13,7 +14,7 @@ def is_browser(headers: CaseInsensitiveDict) -> bool:
return all(key in headers for key in browser_structure)


def setup_browser(filepath=None, headers_raw=None):
def setup_browser(filepath: Optional[str] = None, headers_raw: Optional[str] = None) -> str:
contents = []
if not headers_raw:
eof = "Ctrl-D" if platform.system() != "Windows" else "'Enter, Ctrl-Z, Enter'"
Expand Down
76 changes: 43 additions & 33 deletions ytmusicapi/auth/oauth/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import time
from typing import Dict, Optional
from abc import ABC
from typing import Mapping, Optional

from requests.structures import CaseInsensitiveDict

Expand All @@ -13,7 +14,7 @@ class Credentials:
client_id: str
client_secret: str

def get_code(self) -> Dict:
def get_code(self) -> Mapping:
raise NotImplementedError()

def token_from_code(self, device_code: str) -> RefreshableTokenDict:
Expand All @@ -23,17 +24,17 @@ def refresh_token(self, refresh_token: str) -> BaseTokenDict:
raise NotImplementedError()


class Token:
class Token(ABC):
"""Base class representation of the YouTubeMusicAPI OAuth token."""

access_token: str
refresh_token: str
expires_in: int
expires_at: int
is_expiring: bool
_access_token: str
_refresh_token: str
_expires_in: int
_expires_at: int
_is_expiring: bool

scope: DefaultScope
token_type: Bearer
_scope: DefaultScope
_token_type: Bearer

def __repr__(self) -> str:
"""Readable version."""
Expand All @@ -57,6 +58,34 @@ def as_auth(self) -> str:
"""Returns Authorization header ready str of token_type and access_token."""
return f"{self.token_type} {self.access_token}"

@property
def access_token(self) -> str:
return self._access_token

@property
def refresh_token(self) -> str:
return self._refresh_token

@property
def token_type(self) -> Bearer:
return self._token_type

@property
def scope(self) -> DefaultScope:
return self._scope

@property
def expires_at(self) -> int:
return self._expires_at

@property
def expires_in(self) -> int:
return self._expires_in

@property
def is_expiring(self) -> bool:
return self.expires_in < 60


class OAuthToken(Token):
"""Wrapper for an OAuth token implementing expiration methods."""
Expand All @@ -68,7 +97,7 @@ def __init__(
scope: str,
token_type: str,
expires_at: Optional[int] = None,
expires_in: Optional[int] = None,
expires_in: int = 0,
):
"""

Expand All @@ -84,10 +113,11 @@ def __init__(
self._access_token = access_token
self._refresh_token = refresh_token
self._scope = scope
self._token_type = token_type

# set/calculate token expiration using current epoch
self._expires_at: int = expires_at if expires_at else int(time.time() + expires_in)
self._token_type = token_type
self._expires_at: int = expires_at if expires_at else int(time.time()) + expires_in
self._expires_in: int = expires_in

@staticmethod
def is_oauth(headers: CaseInsensitiveDict) -> bool:
Expand All @@ -109,26 +139,6 @@ def update(self, fresh_access: BaseTokenDict):
self._access_token = fresh_access["access_token"]
self._expires_at = int(time.time() + fresh_access["expires_in"])

@property
def access_token(self) -> str:
return self._access_token

@property
def refresh_token(self) -> str:
return self._refresh_token

@property
def token_type(self) -> Bearer:
return self._token_type

@property
def scope(self) -> DefaultScope:
return self._scope

@property
def expires_at(self) -> int:
return self._expires_at

@property
def expires_in(self) -> int:
return int(self.expires_at - time.time())
Expand Down
9 changes: 4 additions & 5 deletions ytmusicapi/auth/oauth/refreshing.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def __init__(self, token: OAuthToken, credentials: Credentials, local_cache: Opt
# values to new file location via setter
self._local_cache = local_cache

@property
def token_type(self) -> Bearer:
return self.token.token_type

@property
def local_cache(self) -> str | None:
return self._local_cache
Expand Down Expand Up @@ -78,11 +82,6 @@ def store_token(self, path: Optional[str] = None) -> None:
with open(file_path, encoding="utf8", mode="w") as file:
json.dump(self.token.as_dict(), file, indent=True)

@property
def token_type(self) -> Bearer:
# pass underlying value
return self.token.token_type

def as_dict(self) -> RefreshableTokenDict:
# override base class method with call to underlying token's method
return self.token.as_dict()
28 changes: 28 additions & 0 deletions ytmusicapi/mixins/_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""protocol that defines the functions available to mixins"""
from typing import Dict, Optional, Protocol

from requests import Response

from ytmusicapi.auth.types import AuthType
from ytmusicapi.parsers.i18n import Parser


class MixinProtocol(Protocol):
"""protocol that defines the functions available to mixins"""

auth_type: AuthType

parser: Parser

headers: Dict[str, str]

proxies: Optional[Dict[str, str]]

def _check_auth(self) -> None:
pass

def _send_request(self, endpoint: str, body: Dict, additionalParams: str = "") -> Dict:
pass

def _send_get_request(self, url: str, params: Optional[Dict] = None) -> Response:
pass
Loading