diff --git a/.github/actions/init/action.yml b/.github/actions/init/action.yml
new file mode 100644
index 00000000..e2e77d3f
--- /dev/null
+++ b/.github/actions/init/action.yml
@@ -0,0 +1,19 @@
+name: Init
+description: Sets up the environment for build, lint, and test jobs
+
+runs:
+ using: composite
+ steps:
+ - uses: actions/setup-python@v4
+ with:
+ cache: pip
+ cache-dependency-path: |
+ pyproject.toml
+ requirements.txt
+ requirements-dev.txt
+
+ - name: Installing dependencies
+ shell: sh
+ run: >-
+ pip install -U pip &&
+ pip install -U -r requirements.txt -r requirements-dev.txt
diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml
new file mode 100644
index 00000000..6363c7f9
--- /dev/null
+++ b/.github/actions/lint/action.yml
@@ -0,0 +1,23 @@
+name: Lint
+description: Lints the codebase with black, isort, pyflakes, and mypy
+
+runs:
+ using: composite
+ steps:
+ - uses: ./.github/actions/init
+
+ - name: Linting with black
+ run: black --check mnamer tests
+ shell: sh
+
+ - name: Linting isort
+ run: isort --check-only mnamer tests
+ shell: sh
+
+ - name: Linting pyflakes
+ run: pyflakes mnamer tests
+ shell: sh
+
+ - name: Linting mypy
+ run: mypy mnamer tests
+ shell: sh
diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml
new file mode 100644
index 00000000..623eae0d
--- /dev/null
+++ b/.github/actions/test/action.yml
@@ -0,0 +1,46 @@
+name: Test
+description: Tests the codebase with pytest
+
+runs:
+ using: composite
+ steps:
+ - uses: ./.github/actions/init
+
+ - name: Running Local Unit Tests
+ run: >-
+ python -m pytest
+ -m local
+ --durations=10
+ --cov=./
+ --cov-append
+ --cov-report=term-missing
+ --cov-report=xml
+ shell: sh
+
+ - name: Running Network Unit Tests
+ run: >-
+ python -m pytest
+ -m network
+ --reruns 3
+ --durations=10
+ --cov=./
+ --cov-append
+ --cov-report=term-missing
+ --cov-report=xml
+ shell: sh
+
+ - name: Running End to End Tests
+ run: >-
+ python -m pytest
+ -m e2e
+ --reruns 3
+ --durations=10
+ --cov=./
+ --cov-append
+ --cov-report=term-missing
+ --cov-report=xml
+ shell: sh
+
+ - name: Reporting Coverage Statistics
+ uses: codecov/codecov-action@v1
+ if: success() && github.ref == 'refs/heads/main'
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 05804ec1..28fe2693 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -3,43 +3,16 @@ name: pr
on: pull_request
jobs:
-
- lint: # ------------------------------------------------------------------------------
+ lint:
runs-on: ubuntu-latest
steps:
- - name: Checking out Git Commit
- uses: actions/checkout@v2
-
- - name: Installing Python 3
- uses: actions/setup-python@v2
- with:
- python-version: 3.x
- cache: pip
- cache-dependency-path: pyproject.toml
-
- - name: Installing Linters from PyPI
- run: pip install .[test,dev]
-
- - name: Linting with black
- run: black --check mnamer tests
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/lint
- - name: Linting isort
- run: isort --check-only mnamer tests
-
- - name: Linting mypy
- run: mypy mnamer
-
- test: # ------------------------------------------------------------------------------
+ test:
runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-python@v2
- with:
- python-version: 3.x
- cache: pip
- cache-dependency-path: pyproject.toml
- - run: pip install -q -U pip .[test]
- - run: >-
- python -m pytest -m 'not tvdb'
+ steps:
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/test
diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index cab215ab..b1cfcc0b 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -3,154 +3,45 @@ name: push
on:
push:
schedule:
- - cron: "0 8 * * 1" # Mondays at 8am
+ - cron: "0 8 * * 1" # mondays at 8am
jobs:
- build: # -----------------------------------------------------------------------------
+ lint:
runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version: ["3.8", "3.x"]
- name: build-v${{matrix.python-version}}
-
steps:
- - name: Checking out Git Commit
- uses: actions/checkout@v2
-
- - name: Installing Python
- uses: actions/setup-python@v2
- with:
- python-version: ${{matrix.python-version}}
- cache: pip
- cache-dependency-path: pyproject.toml
-
- - uses: actions/cache@v2
- with:
- path: ${{ env.pythonLocation }}
- key: ${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('MANIFEST.in') }}-${{ hashFiles('pyproject.toml') }}
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/lint
- - name: Installing dependencies
- run: pip install -U -r requirements.txt -r requirements-test.txt -r requirements-dev.txt
-
- - name: Attempting build
- run: python -m build --wheel --no-isolation
-
- lint: # ------------------------------------------------------------------------------
+ test:
runs-on: ubuntu-latest
- needs: build
steps:
- - name: Checking out Git Commit
- uses: actions/checkout@v2
-
- - name: Installing Python
- uses: actions/setup-python@v2
- with:
- python-version: 3.x
- cache: pip
- cache-dependency-path: pyproject.toml
+ - uses: actions/checkout@v3
+ - uses: ./.github/actions/test
- - uses: actions/cache@v2
- with:
- path: ${{ env.pythonLocation }}
- key: ${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-text.txt') }}-${{ hashFiles('MANIFEST.in') }}
-
- - name: Linting with black
- run: black --check mnamer tests
-
- - name: Linting isort
- run: isort --check-only mnamer tests
-
- - name: Linting mypy
- run: mypy mnamer
-
- test: # ------------------------------------------------------------------------------
+ publish:
runs-on: ubuntu-latest
- needs: build
-
- strategy:
- matrix:
- python-version: ["8", "3.x"]
- name: test-v${{matrix.python-version}}
- steps:
- - name: Checking out Git Commit
- uses: actions/checkout@v2
-
- - name: Installing Python
- uses: actions/setup-python@v2
- with:
- python-version: ${{matrix.python-version}}
- cache: pip
- cache-dependency-path: pyproject.toml
-
- - uses: actions/cache@v2
- with:
- path: ${{ env.pythonLocation }}
- key: ${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-text.txt') }}-${{ hashFiles('MANIFEST.in') }}
- - name: Running Local Unit Tests
- run: >-
- python -m pytest
- -m local
- --durations=10
- --cov=./
- --cov-append
- --cov-report=term-missing
- --cov-report=xml
-
- - name: Running Network Unit Tests
- run: >-
- python -m pytest
- -m network
- --reruns 3
- --durations=10
- --cov=./
- --cov-append
- --cov-report=term-missing
- --cov-report=xml
+ if: >-
+ success()
+ && github.event_name == 'push'
+ && github.ref == 'refs/heads/main'
- - name: Running End to End Tests
- run: >-
- python -m pytest
- -m e2e
- --reruns 3
- --durations=10
- --cov=./
- --cov-append
- --cov-report=term-missing
- --cov-report=xml
-
- - name: Reporting Coverage Statistics
- if: >
- success()
- && github.event_name == 'push'
- && github.ref == 'refs/heads/master'
- && matrix['python-version'] == '3.x'
- uses: codecov/codecov-action@v1
-
- publish: # ---------------------------------------------------------------------------
- runs-on: ubuntu-latest
- if: startsWith(github.ref, 'refs/tags')
needs:
- - build
- lint
- test
steps:
- - name: Checking out Git Commit
- uses: actions/checkout@v2
-
- - name: Installing Python
- uses: actions/setup-python@v2
- with:
- python-version: 3.8
+ - uses: actions/checkout@v3
+ with: { fetch-depth: 0 } # required for setuptools_scm to detect git tags
+ - uses: ./.github/actions/init
- - name: Installing Requirements
- run: pip install -U -r requirements.txt -r -r requirements-dev.txt
+ - name: Building
+ run: python3 -m build --sdist --wheel --no-isolation
- - name: Building wheel and source distribution
- run: python -m build --sdist --wheel
+ - name: Reporting Version
+ run: python3 -m mnamer --version
- name: Uploading to PyPI
run: >-
diff --git a/.gitignore b/.gitignore
index f6a1f54a..fe565c31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,6 @@ __pycache__/
.idea
.mypy_cache/
.pytest_cache/
-.python-version
.vimrc
.vs
.vscode
@@ -20,6 +19,7 @@ coverage.xml
demo/
dist/
MANIFEST
+mnamer/__version__.py
playground.py
testing/test_files_*
-venv*
\ No newline at end of file
+venv*
diff --git a/.python-version b/.python-version
new file mode 100644
index 00000000..c8cfe395
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.10
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2e6c4db1..02d2fa53 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -14,13 +14,12 @@
"**/.DS_Store": true,
"**/.git": true,
"**/.pytest_cache/": true,
- "**/.svn": true,
"**/__pycache__": true,
"*.egg-info": true,
- "*.egg-info/": true,
".idea": true,
"demo": true,
"venv": true
},
- "python.formatting.provider": "black"
-}
\ No newline at end of file
+ "python.formatting.provider": "black",
+ "python.linting.mypyEnabled": true
+}
diff --git a/Dockerfile b/Dockerfile
index 036de0d0..b93e4670 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
FROM python:alpine
-ARG MNAMER_VERSION=2.4.2
+ARG MNAMER_VERSION=2.5.2
ARG UID=1000
ARG GID=1000
RUN addgroup mnamer -g $GID
diff --git a/README.md b/README.md
index 715c3437..dd6d3237 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
[![PyPI](https://img.shields.io/pypi/v/mnamer.svg?style=for-the-badge)](https://pypi.python.org/pypi/mnamer)
-[![Tests](https://img.shields.io/github/workflow/status/jkwill87/mnamer/push?style=for-the-badge&label=Tests)](https://github.com/jkwill87/mnamer/actions?query=workflow:push)
-[![Coverage](https://img.shields.io/codecov/c/github/jkwill87/mnamer/master.svg?style=for-the-badge)](https://codecov.io/gh/jkwill87/mnamer)
+[![Tests](https://img.shields.io/github/workflow/status/jkwill87/mnamer/push?style=for-the-badge&label=Tests)](https://img.shields.io/github/actions/workflow/status/jkwill87/mnamer/.github/workflows/push.yml?branch=main)
+[![Coverage](https://img.shields.io/codecov/c/github/jkwill87/mnamer/main.svg?style=for-the-badge)](https://codecov.io/gh/jkwill87/mnamer)
[![Licence](https://img.shields.io/github/license/jkwill87/mnamer.svg?style=for-the-badge)](https://en.wikipedia.org/wiki/MIT_License)
[![Style: Black](https://img.shields.io/badge/Style-Black-black.svg?style=for-the-badge)](https://github.com/ambv/black)
-
+
# mnamer
@@ -12,7 +12,7 @@ mnamer (**m**edia re**namer**) is an intelligent and highly configurable media o
Currently it has integration support with [TVDb](https://thetvdb.com) and [TvMaze](https://www.tvmaze.com) for television episodes and [TMDb](https://www.themoviedb.org/) and [OMDb](https://www.omdbapi.com) for movies.
-
+
## Documentation
diff --git a/makefile b/makefile
index ecf8a0bb..acabcb1e 100644
--- a/makefile
+++ b/makefile
@@ -1,7 +1,6 @@
help:
$(info * deployment: build, publish, publish-test)
- $(info * versioning: bump-patch, bump-minor, bump-major)
- $(info * setup: venv, demo)
+ $(info * setup: init, demo)
clean-build:
$(info * cleaning build files)
@@ -12,63 +11,28 @@ clean-demo:
$(info * cleaning demo files)
@rm -rf build dist *.egg-info demo
-clean-venv:
- $(info * removing venv files)
- @deactivate 2> /dev/null || true
- @rm -rf venv
-
-clean: clean-build clean-demo clean-venv
+clean: clean-build clean-demo
# Deployment Helpers -----------------------------------------------------------
-build: clean-build
+init:
+ $(info * installing dependencies)
+ pip3 install -U pip
+ pip3 install -Ur requirements.txt
+ pip3 install -Ur requirements-dev.txt
+
+build: clean-build init
$(info * building distributable)
- @python3 -m build --sdist --wheel --no-isolation > /dev/null 2>&1
+ python3 -m build --sdist --wheel --no-isolation > /dev/null 2>&1
publish: build
$(info * publishing to PyPI repository)
- twine upload --repository-url https://upload.pypi.org/legacy/ dist/*
+ python3 -m twine upload --repository-url https://upload.pypi.org/legacy/ dist/*
publish-test: build
$(info * publishing to PyPI test repository)
- twine upload --repository-url https://test.pypi.org/legacy/ dist/*
-
-
-# Version Helpers --------------------------------------------------------------
-
-bump-patch:
- $(info * resetting any current changes)
- git reset --
- $(info * bumping patch version)
- @vbump --patch
- git add mnamer/__version__.py
- $(info * commiting version change)
- git commit -m "Patch version bump"
- $(info * creating tag)
- git tag `vbump | egrep -o '[0-9].*'`
-
-bump-minor:
- $(info * resetting any current changes)
- git reset --
- $(info * bumping minor version)
- vbump --minor
- git add mnamer/__version__.py
- $(info * commiting version change)
- git commit -m "Minor version bump"
- $(info * creating tag)
- git tag `vbump | egrep -o '[0-9].*'`
-
-bump-major:
- $(info * resetting any current changes)
- git reset --
- $(info * bumping major version)
- vbump --major
- git add mnamer/__version__.py
- $(info * commiting version change)
- git commit -m "Major version bump"
- $(info * creating tag)
- git tag `vbump | egrep -o '[0-9].*'`
+ python3 -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
# Setup Helpers ----------------------------------------------------------------
@@ -107,10 +71,3 @@ demo: clean-demo
"s.w.a.t.2017.s02e01.mkv" \
"scan001.tiff" \
"temp.zip"
-
-venv: clean-venv
- $(info * initializing venv)
- @python3 -m venv venv
- $(info * installing dev requirements)
- @./venv/bin/pip install -qU pip
- @./venv/bin/pip install -qr requirements-dev.txt
diff --git a/mnamer/__version__.py b/mnamer/__version__.py
deleted file mode 100644
index 65fd358a..00000000
--- a/mnamer/__version__.py
+++ /dev/null
@@ -1 +0,0 @@
-VERSION = "2.5.4"
diff --git a/mnamer/argument.py b/mnamer/argument.py
index cf0afbed..42de2ca3 100644
--- a/mnamer/argument.py
+++ b/mnamer/argument.py
@@ -1,12 +1,10 @@
import argparse
-from typing import Any, Dict
+from typing import Any
from mnamer.const import USAGE
from mnamer.setting_spec import SettingSpec
from mnamer.types import SettingType
-__all__ = ["ArgLoader"]
-
HELP_TEMPLATE = """
{usage}
@@ -32,9 +30,8 @@
class ArgLoader(argparse.ArgumentParser):
"""
- An overridden ArgumentParser class which is build to accommodate mnamer's
- setting patterns and delineation of parameter, directive, and positional
- arguments.
+ An overridden ArgumentParser class which is build to accommodate mnamer's setting
+ patterns and delineation of parameter, directive, and positional arguments.
"""
def __init__(self, *specs: SettingSpec):
@@ -70,7 +67,7 @@ def _add_spec(self, spec: SettingSpec):
__iadd__ = _add_spec
- def load(self) -> Dict[str, Any]:
+ def load(self) -> dict[str, Any]:
load_arguments, unknowns = self.parse_known_args()
if unknowns:
raise RuntimeError(f"invalid arguments: {','.join(unknowns)}")
@@ -78,8 +75,8 @@ def load(self) -> Dict[str, Any]:
def format_help(self) -> str:
"""
- Overrides ArgumentParser's format_help to dynamically generate a help
- message for use with the `--help` flag.
+ Overrides ArgumentParser's format_help to dynamically generate a help message
+ for use with the `--help` flag.
"""
def help_for_group(group: SettingType) -> str:
diff --git a/mnamer/const.py b/mnamer/const.py
index cfa22fc4..c6c966a4 100644
--- a/mnamer/const.py
+++ b/mnamer/const.py
@@ -5,6 +5,8 @@
from platform import platform, python_version
from sys import argv, gettrace, version_info
+from setuptools_scm import get_version # type: ignore
+
try:
from appdirs import __version__ as appdirs_version # type: ignore
except ModuleNotFoundError:
@@ -37,20 +39,6 @@
except ModuleNotFoundError:
teletype_version = "N/A"
-from mnamer.__version__ import VERSION
-
-__all__ = [
- "CACHE_PATH",
- "CURRENT_YEAR",
- "DEPRECATED",
- "IS_DEBUG",
- "SUBTITLE_CONTAINERS",
- "SYSTEM",
- "USAGE",
- "VERSION",
- "VERSION_MAJOR",
-]
-
CACHE_PATH = Path(
cache_dir, f"mnamer-py{version_info.major}.{version_info.minor}"
@@ -64,6 +52,8 @@
SUBTITLE_CONTAINERS = [".srt", ".idx", ".sub"]
+VERSION = get_version(root="..", relative_to=__file__, local_scheme="dirty-tag")
+
SYSTEM = {
"date": dt.date.today(),
"platform": platform(),
@@ -79,5 +69,3 @@
}
USAGE = "USAGE: mnamer [preferences] [directives] target [targets ...]"
-
-VERSION_MAJOR = int(VERSION[0])
diff --git a/mnamer/endpoints.py b/mnamer/endpoints.py
index 19a41353..1cbab96f 100644
--- a/mnamer/endpoints.py
+++ b/mnamer/endpoints.py
@@ -3,7 +3,6 @@
import datetime
from re import match
from time import sleep
-from typing import Optional, Union
from mnamer.exceptions import (
MnamerException,
@@ -13,41 +12,19 @@
from mnamer.language import Language
from mnamer.utils import clean_dict, parse_date, request_json
-__all__ = [
- "omdb_search",
- "omdb_title",
- "tmdb_find",
- "tmdb_movies",
- "tmdb_search_movies",
- "tvdb_episodes_id",
- "tvdb_login",
- "tvdb_refresh_token",
- "tvdb_search_series",
- "tvdb_series_id_episodes_query",
- "tvdb_series_id_episodes",
- "tvdb_series_id",
- "tvmaze_episode_by_number",
- "tvmaze_episodes_by_date",
- "tvmaze_show",
- "tvmaze_show_episodes_list",
- "tvmaze_show_lookup",
- "tvmaze_show_search",
- "tvmaze_show_single_search",
-]
-
OMDB_PLOT_TYPES = {"short", "long"}
MAX_RETRIES = 5
def omdb_title(
api_key: str,
- id_imdb: Optional[str] = None,
- media: Optional[str] = None,
- title: Optional[str] = None,
- season: Optional[int] = None,
- episode: Optional[int] = None,
- year: Optional[int] = None,
- plot: Optional[str] = None,
+ id_imdb: str | None = None,
+ media: str | None = None,
+ title: str | None = None,
+ season: int | None = None,
+ episode: int | None = None,
+ year: int | None = None,
+ plot: str | None = None,
cache: bool = True,
) -> dict:
"""
@@ -87,8 +64,8 @@ def omdb_title(
def omdb_search(
api_key: str,
query: str,
- year: Optional[int] = None,
- media: Optional[str] = None,
+ year: int | None = None,
+ media: str | None = None,
page: int = 1,
cache: bool = True,
) -> dict:
@@ -122,7 +99,7 @@ def tmdb_find(
api_key: str,
external_source: str,
external_id: str,
- language: Optional[Language] = None,
+ language: Language | None = None,
cache: bool = True,
) -> dict:
"""
@@ -165,7 +142,7 @@ def tmdb_find(
def tmdb_movies(
api_key: str,
id_tmdb: str,
- language: Optional[Language] = None,
+ language: Language | None = None,
cache: bool = True,
) -> dict:
"""
@@ -188,9 +165,9 @@ def tmdb_movies(
def tmdb_search_movies(
api_key: str,
title: str,
- year: Optional[Union[int, str]] = None,
- language: Optional[Language] = None,
- region: Optional[str] = None,
+ year: int | str | None = None,
+ language: Language | None = None,
+ region: str | None = None,
adult: bool = False,
page: int = 1,
cache: bool = True,
@@ -220,7 +197,7 @@ def tmdb_search_movies(
return content
-def tvdb_login(api_key: Optional[str]) -> str:
+def tvdb_login(api_key: str | None) -> str:
"""
Logs into TVDb using the provided api key.
@@ -256,7 +233,7 @@ def tvdb_refresh_token(token: str) -> str:
def tvdb_episodes_id(
token: str,
id_tvdb: str,
- language: Optional[Language] = None,
+ language: Language | None = None,
cache: bool = True,
) -> dict:
"""
@@ -286,7 +263,7 @@ def tvdb_episodes_id(
def tvdb_series_id(
token: str,
id_tvdb: str,
- language: Optional[Language] = None,
+ language: Language | None = None,
cache: bool = True,
) -> dict:
"""
@@ -318,7 +295,7 @@ def tvdb_series_id_episodes(
token: str,
id_tvdb: str,
page: int = 1,
- language: Optional[Language] = None,
+ language: Language | None = None,
cache: bool = True,
) -> dict:
"""
@@ -348,10 +325,10 @@ def tvdb_series_id_episodes(
def tvdb_series_id_episodes_query(
token: str,
id_tvdb: str,
- episode: Optional[int] = None,
- season: Optional[int] = None,
+ episode: int | None = None,
+ season: int | None = None,
page: int = 1,
- language: Optional[Language] = None,
+ language: Language | None = None,
cache: bool = True,
) -> dict:
"""
@@ -384,10 +361,10 @@ def tvdb_series_id_episodes_query(
def tvdb_search_series(
token: str,
- series: Optional[str] = None,
- id_imdb: Optional[str] = None,
- id_zap2it: Optional[str] = None,
- language: Optional[Language] = None,
+ series: str | None = None,
+ id_imdb: str | None = None,
+ id_zap2it: str | None = None,
+ language: Language | None = None,
cache: bool = True,
) -> dict:
"""
@@ -489,8 +466,8 @@ def tvmaze_show_single_search(query: str, cache: bool = True, attempt: int = 1)
def tvmaze_show_lookup(
- id_imdb: Optional[str] = None,
- id_tvdb: Optional[str] = None,
+ id_imdb: str | None = None,
+ id_tvdb: str | None = None,
cache: bool = True,
attempt: int = 1,
) -> dict:
@@ -545,7 +522,7 @@ def tvmaze_show_episodes_list(
def tvmaze_episodes_by_date(
id_tvmaze: str,
- air_date: Union[datetime.date, str],
+ air_date: datetime.date | str,
cache: bool = True,
attempt: int = 1,
) -> dict:
@@ -570,8 +547,8 @@ def tvmaze_episodes_by_date(
def tvmaze_episode_by_number(
id_tvmaze: str,
- season: Optional[int],
- episode: Optional[int],
+ season: int | None,
+ episode: int | None,
cache: bool = True,
attempt: int = 1,
) -> dict:
diff --git a/mnamer/frontends.py b/mnamer/frontends.py
index 39c2de9b..0f47a6ac 100644
--- a/mnamer/frontends.py
+++ b/mnamer/frontends.py
@@ -1,5 +1,4 @@
from abc import ABC, abstractmethod
-from typing import List
from mnamer import tty
from mnamer.const import SYSTEM, USAGE, VERSION
@@ -18,7 +17,7 @@
class Frontend(ABC):
settings: SettingStore
- targets: List[Target]
+ targets: list[Target]
def __init__(self, settings: SettingStore):
self.settings = settings
@@ -152,7 +151,7 @@ def _process_targets(self) -> None:
self._rename_and_move_file(target)
def _announce_file(self, target: Target):
- media_type = target.metadata.media.value.title()
+ media_type = target.metadata.to_media_type().value.title()
description = (
f"{media_type} Subtitle"
if is_subtitle(target.metadata.container)
diff --git a/mnamer/language.py b/mnamer/language.py
index 7548fa6f..afe520ee 100644
--- a/mnamer/language.py
+++ b/mnamer/language.py
@@ -1,11 +1,11 @@
from __future__ import annotations
import dataclasses
-from typing import Any, Optional, Tuple
+from typing import Any
from mnamer.exceptions import MnamerException
-_KNOWN = (
+KNOWN_LANGUAGES = (
("arabic", "ar", "ara"),
("chinese", "zh", "zho"),
("croatian", "hr", "hrv"),
@@ -41,7 +41,7 @@ class Language:
a3: str
@classmethod
- def parse(cls, value: Any) -> Optional[Language]:
+ def parse(cls, value: Any) -> Language | None:
if not value:
return None
if isinstance(value, cls):
@@ -56,21 +56,23 @@ def parse(cls, value: Any) -> Optional[Language]:
except:
raise MnamerException("Could not determine language")
value = value.lower()
- for row in _KNOWN:
+ for row in KNOWN_LANGUAGES:
for item in row:
if value == item:
return cls(row[0].capitalize(), row[1], row[2])
raise MnamerException("Could not determine language")
@classmethod
- def all(cls) -> Tuple[Language, ...]:
- return tuple(cls(row[0].capitalize(), row[1], row[2]) for row in _KNOWN)
+ def all(cls) -> tuple[Language, ...]:
+ return tuple(
+ cls(row[0].capitalize(), row[1], row[2]) for row in KNOWN_LANGUAGES
+ )
def __str__(self) -> str:
return self.a2
@staticmethod
- def ensure_valid_for_tvdb(language: Optional[Language]):
+ def ensure_valid_for_tvdb(language: Language | None):
valid = {
"cs",
"da",
diff --git a/mnamer/metadata.py b/mnamer/metadata.py
index f035da9b..df43382a 100644
--- a/mnamer/metadata.py
+++ b/mnamer/metadata.py
@@ -1,8 +1,10 @@
+from __future__ import annotations
+
import dataclasses
import datetime as dt
import re
from string import Formatter
-from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Union
+from typing import Any, Callable, Mapping, Sequence
from mnamer.language import Language
from mnamer.types import MediaType
@@ -17,16 +19,14 @@
year_parse,
)
-__all__ = ["Metadata", "MetadataMovie", "MetadataEpisode"]
-
class _MetaFormatter(Formatter):
- def format_field(self, value: Union[None, int, str], format_spec: str) -> str:
+ def format_field(self, value: None | int | str, format_spec: str) -> str:
return format(value, format_spec) if value is not None else ""
def get_value(
- self, key: Union[str, int], args: Sequence[Any], kwargs: Mapping[str, Any]
- ) -> Union[None, int, str]:
+ self, key: str | int, args: Sequence[Any], kwargs: Mapping[str, Any]
+ ) -> None | int | str:
if isinstance(key, int):
assert args
return args[key]
@@ -38,16 +38,24 @@ def get_value(
class Metadata:
"""A dataclass which transforms and stores media metadata information."""
- container: Optional[str] = None
- group: Optional[str] = None
- language: Optional[Language] = None
- language_sub: Optional[Language] = None
- quality: Optional[str] = None
- synopsis: Optional[str] = None
- media: Union[MediaType, str, None] = None
+ container: str | None = None
+ group: str | None = None
+ language: Language | None = None
+ language_sub: Language | None = None
+ quality: str | None = None
+ synopsis: str | None = None
+
+ @classmethod
+ def to_media_type(cls) -> MediaType:
+ if cls is MetadataEpisode:
+ return MediaType.EPISODE
+ elif cls is MetadataMovie:
+ return MediaType.MOVIE
+ else:
+ raise ValueError(f"Unknown metadata class: {cls}")
def __setattr__(self, key: str, value: Any):
- converter_map: Dict[str, Callable] = {
+ converter_map: dict[str, Callable] = {
"container": normalize_container,
"group": str.upper,
"language": Language.parse,
@@ -56,12 +64,12 @@ def __setattr__(self, key: str, value: Any):
"quality": str.lower,
"synopsis": str.capitalize,
}
- converter: Optional[Callable] = converter_map.get(key)
+ converter: Callable | None = converter_map.get(key)
if value is not None and converter:
value = converter(value)
super().__setattr__(key, value)
- def __format__(self, format_spec: Optional[str]):
+ def __format__(self, format_spec: str | None):
raise NotImplementedError
def __str__(self) -> str:
@@ -74,7 +82,7 @@ def extension(self):
else:
return self.container
- def as_dict(self) -> Dict[str, Any]:
+ def as_dict(self) -> dict[str, Any]:
d = dataclasses.asdict(self)
d["extension"] = self.extension
return d
@@ -86,7 +94,7 @@ def _format_repl(self, mobj) -> str:
value = str_title_case(value)
return value
- def update(self, metadata: "Metadata"):
+ def update(self, metadata: Metadata):
"""Overlays all none value from another Metadata instance."""
for field in dataclasses.asdict(self).keys():
value = getattr(metadata, field)
@@ -102,13 +110,12 @@ class MetadataMovie(Metadata):
to movies.
"""
- name: Optional[str] = None
- year: Optional[str] = None
- id_imdb: Optional[str] = None
- id_tmdb: Union[str, None] = None
- media: MediaType = MediaType.MOVIE
+ name: str | None = None
+ year: str | None = None
+ id_imdb: str | None = None
+ id_tmdb: str | None = None
- def __format__(self, format_spec: Optional[str]):
+ def __format__(self, format_spec: str | None):
default = "{name} ({year})"
re_pattern = r"({(\w+)(?:\[[\w:]+\])?(?:\:\d{1,2})?})"
s = re.sub(re_pattern, self._format_repl, format_spec or default)
@@ -116,11 +123,11 @@ def __format__(self, format_spec: Optional[str]):
return s
def __setattr__(self, key: str, value: Any):
- converter_map: Dict[str, Callable] = {
+ converter_map: dict[str, Callable] = {
"name": fn_pipe(str_replace_slashes, str_title_case),
"year": year_parse,
}
- converter: Optional[Callable] = converter_map.get(key)
+ converter: Callable | None = converter_map.get(key)
if value is not None and converter:
value = converter(value)
super().__setattr__(key, value)
@@ -129,18 +136,17 @@ def __setattr__(self, key: str, value: Any):
@dataclasses.dataclass
class MetadataEpisode(Metadata):
"""
- A dataclass which transforms and stores media metadata information specific
- to television episodes.
+ A dataclass which transforms and stores media metadata information specific to
+ television episodes.
"""
- series: Optional[str] = None
- season: Optional[int] = None
- episode: Optional[int] = None
- date: Optional[dt.date] = None
- title: Optional[str] = None
- id_tvdb: Optional[str] = None
- id_tvmaze: Optional[str] = None
- media: MediaType = MediaType.EPISODE
+ series: str | None = None
+ season: int | None = None
+ episode: int | None = None
+ date: dt.date | None = None
+ title: str | None = None
+ id_tvdb: str | None = None
+ id_tvmaze: str | None = None
def __post_init__(self):
if isinstance(self.season, str):
@@ -150,7 +156,7 @@ def __post_init__(self):
if isinstance(self.date, str):
self.date = parse_date(self.date)
- def __format__(self, format_spec: Optional[str]):
+ def __format__(self, format_spec: str | None):
default = "{series} - {season:02}x{episode:02} - {title}"
re_pattern = r"({(\w+)(?:\[[\w:]+\])?(?:\:\d{1,2})?})"
s = re.sub(re_pattern, self._format_repl, format_spec or default)
@@ -158,14 +164,14 @@ def __format__(self, format_spec: Optional[str]):
return s
def __setattr__(self, key: str, value: Any):
- converter_map: Dict[str, Callable] = {
+ converter_map: dict[str, Callable] = {
"date": parse_date,
"episode": int,
"season": int,
"series": fn_pipe(str_replace_slashes, str_title_case),
"title": fn_pipe(str_replace_slashes, str_title_case),
}
- converter: Optional[Callable] = converter_map.get(key)
+ converter: Callable | None = converter_map.get(key)
if value is not None and converter:
value = converter(value)
super().__setattr__(key, value)
diff --git a/mnamer/providers.py b/mnamer/providers.py
index 010fc633..a650f893 100644
--- a/mnamer/providers.py
+++ b/mnamer/providers.py
@@ -5,7 +5,7 @@
import datetime as dt
from abc import ABC, abstractmethod
from os import environ
-from typing import Iterator, Optional, TypeVar
+from typing import Iterator
from mnamer.endpoints import (
omdb_search,
@@ -30,16 +30,14 @@
from mnamer.types import MediaType, ProviderType
from mnamer.utils import parse_date, year_range_parse
-__all__ = ["Provider", "Omdb", "Tmdb", "Tvdb", "TvMaze"]
-
class Provider(ABC):
"""ABC for Providers, high-level interfaces for metadata media providers."""
- api_key: Optional[str] = None
+ api_key: str | None = None
cache: bool = True
- def __init__(self, api_key: Optional[str] = None, cache: bool = True):
+ def __init__(self, api_key: str | None = None, cache: bool = True):
"""Initializes the provider."""
if api_key:
self.api_key = api_key
@@ -75,7 +73,7 @@ class Omdb(Provider):
api_key: str = environ.get("API_KEY_OMDB", "477a7ebc")
- def __init__(self, api_key: Optional[str] = None, cache: bool = True):
+ def __init__(self, api_key: str | None = None, cache: bool = True):
super().__init__(api_key, cache)
assert self.api_key
@@ -112,7 +110,7 @@ def _lookup_movie(self, id_imdb: str) -> Iterator[MetadataMovie]:
meta.synopsis = None
yield meta
- def _search_movie(self, name: str, year: Optional[str]) -> Iterator[MetadataMovie]:
+ def _search_movie(self, name: str, year: str | None) -> Iterator[MetadataMovie]:
assert self.api_key
year_from, year_to = year_range_parse(year, 5)
found = False
@@ -145,7 +143,7 @@ class Tmdb(Provider):
api_key: str = environ.get("API_KEY_TMDB", "db972a607f2760bb19ff8bb34074b4c7")
- def __init__(self, api_key: Optional[str] = None, cache: bool = True):
+ def __init__(self, api_key: str | None = None, cache: bool = True):
super().__init__(api_key, cache)
assert self.api_key
@@ -161,7 +159,7 @@ def search(self, query: MetadataMovie) -> Iterator[MetadataMovie]:
yield from results
def _search_id(
- self, id_tmdb: str, language: Optional[Language] = None
+ self, id_tmdb: str, language: Language | None = None
) -> Iterator[MetadataMovie]:
assert self.api_key
response = tmdb_movies(self.api_key, id_tmdb, language, self.cache)
@@ -174,9 +172,7 @@ def _search_id(
id_imdb=response["imdb_id"],
)
- def _search_name(
- self, name: str, year: Optional[str], language: Optional[Language]
- ):
+ def _search_name(self, name: str, year: str | None, language: Language | None):
assert self.api_key
year_from, year_to = year_range_parse(year, 5)
page = 1
@@ -221,7 +217,7 @@ class Tvdb(Provider):
api_key: str = environ.get("API_KEY_TVDB", "E69C7A2CEF2F3152")
- def __init__(self, api_key: Optional[str] = None, cache: bool = True):
+ def __init__(self, api_key: str | None = None, cache: bool = True):
super().__init__(api_key, cache)
assert self.api_key
self.token = "" if self.cache else self._login()
@@ -254,9 +250,9 @@ def search(self, query: MetadataEpisode) -> Iterator[MetadataEpisode]:
def _search_id(
self,
id_tvdb: str,
- season: Optional[int] = None,
- episode: Optional[int] = None,
- language: Optional[Language] = None,
+ season: int | None = None,
+ episode: int | None = None,
+ language: Language | None = None,
):
found = False
series_data = tvdb_series_id(
@@ -300,9 +296,9 @@ def _search_id(
def _search_series(
self,
series: str,
- season: Optional[int],
- episode: Optional[int],
- language: Optional[Language],
+ season: int | None,
+ episode: int | None,
+ language: Language | None,
):
found = False
series_data = tvdb_search_series(
@@ -322,7 +318,7 @@ def _search_series(
raise MnamerNotFoundException
def _search_tvdb_date(
- self, id_tvdb: str, release_date: dt.date, language: Optional[Language]
+ self, id_tvdb: str, release_date: dt.date, language: Language | None
):
release_date = parse_date(release_date)
found = False
@@ -334,7 +330,7 @@ def _search_tvdb_date(
raise MnamerNotFoundException
def _search_series_date(
- self, series: str, release_date: dt.date, language: Optional[Language]
+ self, series: str, release_date: dt.date, language: Language | None
):
release_date = parse_date(release_date)
series_data = tvdb_search_series(
@@ -380,7 +376,7 @@ def search(self, query: MetadataEpisode) -> Iterator[MetadataEpisode]:
raise MnamerNotFoundException
def _lookup_with_tmaze_id_and_season_and_episode(
- self, id_tvmaze: str, season: Optional[int], episode: Optional[int]
+ self, id_tvmaze: str, season: int | None, episode: int | None
) -> Iterator[MetadataEpisode]:
series_data = tvmaze_show(id_tvmaze)
episode_data = tvmaze_episode_by_number(id_tvmaze, season, episode)
@@ -388,7 +384,7 @@ def _lookup_with_tmaze_id_and_season_and_episode(
yield self._transform_meta(id_tvmaze, id_tvdb, series_data, episode_data)
def _lookup_with_id_and_date(
- self, id_tvmaze: Optional[str], id_tvdb: Optional[str], air_date: dt.date
+ self, id_tvmaze: str | None, id_tvdb: str | None, air_date: dt.date
) -> Iterator[MetadataEpisode]:
assert id_tvmaze or id_tvdb
if id_tvmaze:
@@ -407,10 +403,10 @@ def _lookup_with_id_and_date(
def _lookup_with_id(
self,
- id_tvmaze: Optional[str],
- id_tvdb: Optional[str],
- season: Optional[int],
- episode: Optional[int],
+ id_tvmaze: str | None,
+ id_tvdb: str | None,
+ season: int | None,
+ episode: int | None,
) -> Iterator[MetadataEpisode]:
assert id_tvmaze or id_tvdb
if id_tvmaze:
@@ -433,7 +429,7 @@ def _lookup_with_id(
yield meta
def _search_with_season_and_episode(
- self, series: str, season: Optional[int], episode: Optional[int]
+ self, series: str, season: int | None, episode: int | None
) -> Iterator[MetadataEpisode]:
assert season
series_data = tvmaze_show_search(series)
@@ -454,7 +450,7 @@ def _search_with_season_and_episode(
yield meta
def _search(
- self, series: str, season: Optional[int], episode: Optional[int]
+ self, series: str, season: int | None, episode: int | None
) -> Iterator[MetadataEpisode]:
assert series
series_data = tvmaze_show_search(series)
@@ -477,7 +473,7 @@ def _search(
@staticmethod
def _transform_meta(
- id_tvmaze: str, id_tvdb: Optional[str], series_entry: dict, episode_entry: dict
+ id_tvmaze: str, id_tvdb: str | None, series_entry: dict, episode_entry: dict
) -> MetadataEpisode:
return MetadataEpisode(
date=episode_entry["airdate"] or None,
diff --git a/mnamer/setting_spec.py b/mnamer/setting_spec.py
index 9e09c10f..258ed1d3 100644
--- a/mnamer/setting_spec.py
+++ b/mnamer/setting_spec.py
@@ -1,10 +1,8 @@
import dataclasses
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any
from mnamer.types import SettingType
-__all__ = ["SettingSpec"]
-
@dataclasses.dataclass(frozen=True)
class SettingSpec:
@@ -14,22 +12,22 @@ class SettingSpec:
"""
group: SettingType
- dest: Optional[str] = None
- action: Optional[str] = None
- choices: Optional[List[str]] = None
- flags: Optional[List[str]] = None
- help: Optional[str] = None
- nargs: Optional[str] = None
- type: Optional[type] = None
-
- def as_dict(self) -> Dict[str, Any]:
+ dest: str | None = None
+ action: str | None = None
+ choices: list[str] | None = None
+ flags: list[str] | None = None
+ help: str | None = None
+ nargs: str | None = None
+ typevar: type | None = None
+
+ def as_dict(self) -> dict[str, Any]:
"""Converts ArgSpec instance into a Python dictionary."""
return {k: v for k, v in vars(self).items() if k}
__call__ = as_dict
@property
- def registration(self) -> Tuple[List[str], Dict[str, Any]]:
+ def registration(self) -> tuple[list[str], dict[str, Any]]:
names = self.flags or []
options = {
"action": self.action,
@@ -38,12 +36,12 @@ def registration(self) -> Tuple[List[str], Dict[str, Any]]:
"dest": self.dest,
"help": self.help,
"nargs": self.nargs,
- "type": self.type,
+ "type": self.typevar,
}
return names, {k: v for k, v in options.items() if v is not None}
@property
- def name(self) -> Optional[str]:
+ def name(self) -> str | None:
if self.flags:
return max(self.flags, key=len).lstrip("-")
return None
diff --git a/mnamer/setting_store.py b/mnamer/setting_store.py
index ce6d4d7a..8981d335 100644
--- a/mnamer/setting_store.py
+++ b/mnamer/setting_store.py
@@ -1,18 +1,17 @@
import dataclasses
import json
from pathlib import Path
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Callable
from mnamer.argument import ArgLoader
from mnamer.const import SUBTITLE_CONTAINERS
from mnamer.exceptions import MnamerException
from mnamer.language import Language
+from mnamer.metadata import Metadata
from mnamer.setting_spec import SettingSpec
from mnamer.types import MediaType, ProviderType, SettingType
from mnamer.utils import crawl_out, json_loads, normalize_containers
-__all__ = ["SettingStore"]
-
@dataclasses.dataclass
class SettingStore:
@@ -23,7 +22,7 @@ class SettingStore:
# positional attributes ----------------------------------------------------
- targets: List[Path] = dataclasses.field(
+ targets: list[Path] = dataclasses.field(
default_factory=lambda: [],
metadata=SettingSpec(
flags=["targets"],
@@ -87,10 +86,10 @@ class SettingStore:
flags=["--hits"],
group=SettingType.PARAMETER,
help="--hits=: limit the maximum number of hits for each query",
- type=int,
+ typevar=int,
).as_dict(),
)
- ignore: List[str] = dataclasses.field(
+ ignore: list[str] = dataclasses.field(
default_factory=lambda: [".*sample.*", "^RARBG.*"],
metadata=SettingSpec(
flags=["--ignore"],
@@ -99,7 +98,7 @@ class SettingStore:
nargs="+",
).as_dict(),
)
- language: Optional[Language] = dataclasses.field(
+ language: Language | None = dataclasses.field(
default=None,
metadata=SettingSpec(
flags=["--language"],
@@ -107,7 +106,7 @@ class SettingStore:
help="--language=: specify the search language",
).as_dict(),
)
- mask: List[str] = dataclasses.field(
+ mask: list[str] = dataclasses.field(
default_factory=lambda: [
"avi",
"m4v",
@@ -154,7 +153,7 @@ class SettingStore:
help="--no-style: print to stdout without using colour or unicode chars",
).as_dict(),
)
- movie_api: Union[ProviderType, str] = dataclasses.field(
+ movie_api: ProviderType | str = dataclasses.field(
default=ProviderType.TMDB,
metadata=SettingSpec(
choices=[ProviderType.TMDB.value, ProviderType.OMDB.value],
@@ -164,7 +163,7 @@ class SettingStore:
help="--movie-api={*tmdb,omdb}: set movie api provider",
).as_dict(),
)
- movie_directory: Optional[Path] = dataclasses.field(
+ movie_directory: Path | None = dataclasses.field(
default=None,
metadata=SettingSpec(
dest="movie_directory",
@@ -186,7 +185,7 @@ class SettingStore:
help="--movie-format: set movie renaming format specification",
).as_dict(),
)
- episode_api: Union[ProviderType, str] = dataclasses.field(
+ episode_api: ProviderType | str = dataclasses.field(
default=ProviderType.TVMAZE,
metadata=SettingSpec(
choices=[ProviderType.TVDB.value, ProviderType.TVMAZE.value],
@@ -196,7 +195,7 @@ class SettingStore:
help="--episode-api={tvdb,*tvmaze}: set episode api provider",
).as_dict(),
)
- episode_directory: Optional[Path] = dataclasses.field(
+ episode_directory: Path | None = dataclasses.field(
default=None,
metadata=SettingSpec(
dest="episode_directory",
@@ -260,7 +259,7 @@ class SettingStore:
help="--config-ignore: skips loading config file for session",
).as_dict(),
)
- config_path: Optional[str] = dataclasses.field(
+ config_path: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(
flags=["--config_path", "--config-path"],
@@ -268,7 +267,7 @@ class SettingStore:
help="--config-path=: specifies configuration path to load",
).as_dict(),
)
- id_imdb: Optional[str] = dataclasses.field(
+ id_imdb: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(
flags=["--id_imdb", "--id-imdb", "--idimdb"],
@@ -276,7 +275,7 @@ class SettingStore:
help="--id-imdb=: specify an IMDb movie id override",
).as_dict(),
)
- id_tmdb: Optional[str] = dataclasses.field(
+ id_tmdb: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(
flags=["--id_tmdb", "--id-tmdb", "--idtmdb"],
@@ -284,7 +283,7 @@ class SettingStore:
help="--id-tmdb=: specify a TMDb movie id override",
).as_dict(),
)
- id_tvdb: Optional[str] = dataclasses.field(
+ id_tvdb: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(
flags=["--id_tvdb", "--id-tvdb", "--idtvdb"],
@@ -292,7 +291,7 @@ class SettingStore:
help="--id-tvdb=: specify a TVDb series id override",
).as_dict(),
)
- id_tvmaze: Optional[str] = dataclasses.field(
+ id_tvmaze: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(
flags=["--id_tvmaze", "--id-tvmaze", "--idtvmaze"],
@@ -310,7 +309,7 @@ class SettingStore:
help="--no-cache: disable request cache",
).as_dict(),
)
- media: Optional[MediaType] = dataclasses.field(
+ media: MediaType | None = dataclasses.field(
default=None,
metadata=SettingSpec(
choices=[MediaType.EPISODE.value, MediaType.MOVIE.value],
@@ -331,33 +330,33 @@ class SettingStore:
# config-only attributes ---------------------------------------------------
- api_key_omdb: Optional[str] = dataclasses.field(
+ api_key_omdb: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(group=SettingType.CONFIGURATION).as_dict(),
)
- api_key_tmdb: Optional[str] = dataclasses.field(
+ api_key_tmdb: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(group=SettingType.CONFIGURATION).as_dict(),
)
- api_key_tvdb: Optional[str] = dataclasses.field(
+ api_key_tvdb: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(group=SettingType.CONFIGURATION).as_dict(),
)
- api_key_tvmaze: Optional[str] = dataclasses.field(
+ api_key_tvmaze: str | None = dataclasses.field(
default=None,
metadata=SettingSpec(group=SettingType.CONFIGURATION).as_dict(),
)
- replace_before: Dict[str, str] = dataclasses.field(
+ replace_before: dict[str, str] = dataclasses.field(
default_factory=lambda: {},
metadata=SettingSpec(group=SettingType.CONFIGURATION).as_dict(),
)
- replace_after: Dict[str, str] = dataclasses.field(
+ replace_after: dict[str, str] = dataclasses.field(
default_factory=lambda: {"&": "and", "@": "at", ";": ","},
metadata=SettingSpec(group=SettingType.CONFIGURATION).as_dict(),
)
@classmethod
- def specifications(cls) -> List[SettingSpec]:
+ def specifications(cls) -> list[SettingSpec]:
return [
SettingSpec(**f.metadata)
for f in dataclasses.fields(SettingStore)
@@ -365,11 +364,11 @@ def specifications(cls) -> List[SettingSpec]:
]
@staticmethod
- def _resolve_path(path: Union[str, Path]) -> Path:
+ def _resolve_path(path: str | Path) -> Path:
return Path(path).resolve()
def __setattr__(self, key: str, value: Any):
- converter_map: Dict[str, Callable] = {
+ converter_map: dict[str, Callable] = {
"episode_api": ProviderType,
"episode_directory": self._resolve_path,
"language": Language.parse,
@@ -379,12 +378,12 @@ def __setattr__(self, key: str, value: Any):
"movie_directory": self._resolve_path,
"targets": lambda targets: [Path(target) for target in targets],
}
- converter: Optional[Callable] = converter_map.get(key)
+ converter: Callable | None = converter_map.get(key)
if value is not None and converter:
value = converter(value)
super().__setattr__(key, value)
- def as_dict(self) -> Dict[str, Any]:
+ def as_dict(self) -> dict[str, Any]:
return dataclasses.asdict(self)
def as_json(self) -> str:
@@ -415,7 +414,7 @@ def as_json(self) -> str:
sort_keys=True,
)
- def bulk_apply(self, d: Dict[str, Any]):
+ def bulk_apply(self, d: dict[str, Any]):
for k, v in d.items():
if v:
setattr(self, k, v)
@@ -434,17 +433,18 @@ def load(self) -> None:
self.bulk_apply(arguments)
return None
- def api_for(self, media_type: Optional[MediaType]) -> Optional[ProviderType]:
+ def api_for(self, media_type: MediaType | None) -> ProviderType | None:
"""Returns the ProviderType for a given media type."""
if media_type:
return getattr(self, f"{media_type.value}_api")
return None
- def api_key_for(self, provider_type: ProviderType) -> Optional[str]:
+ def api_key_for(self, provider_type: ProviderType) -> str | None:
"""Returns the API key for a provider type."""
if provider_type:
return getattr(self, f"api_key_{provider_type.value}")
return None
- def formatting_for(self, media_type: MediaType) -> str:
- return getattr(self, f"{media_type.value}_format") if media_type else ""
+ def formatting_for(self, media: MediaType | Metadata) -> str:
+ """Returns the formatting string for a given media type or metadata."""
+ return getattr(self, f"{ media.to_media_type().value}_format")
diff --git a/mnamer/target.py b/mnamer/target.py
index 9f072892..cd9927e2 100644
--- a/mnamer/target.py
+++ b/mnamer/target.py
@@ -4,7 +4,7 @@
from os import path
from pathlib import Path
from shutil import move
-from typing import Any, ClassVar, Dict, List, Optional, Type
+from typing import Any, ClassVar, Type
from guessit import guessit # type: ignore
@@ -19,29 +19,29 @@
filename_replace,
filter_blacklist,
filter_containers,
+ is_subtitle,
str_replace,
str_sanitize,
str_scenify,
)
-__all__ = ["Target"]
-
class Target:
"""Manages metadata state for a media file and facilitates its relocation."""
- _providers: ClassVar[Dict[ProviderType, Provider]] = {}
+ _providers: ClassVar[dict[ProviderType, Provider]] = {}
_settings: SettingStore
_provider: Provider
_has_moved: bool
_has_renamed: bool
- _raw_metadata: Dict[str, str]
+ _raw_metadata: dict[str, str]
_parsed_metadata: Metadata
source: Path
+ metadata: Metadata
- def __init__(self, file_path: Path, settings: SettingStore = None):
+ def __init__(self, file_path: Path, settings: SettingStore | None = None):
self.source = file_path
self._settings = settings or SettingStore()
self._has_moved = False
@@ -58,7 +58,7 @@ def __str__(self) -> str:
return str(self.source)
@classmethod
- def populate_paths(cls: Type[Target], settings: SettingStore) -> List[Target]:
+ def populate_paths(cls: Type[Target], settings: SettingStore) -> list[Target]:
"""Creates a list of Target objects for media files found in paths."""
file_paths = crawl_in(settings.targets, settings.recurse)
file_paths = filter_blacklist(file_paths, settings.ignore)
@@ -73,21 +73,22 @@ def reset_providers(cls):
cls._providers.clear()
@staticmethod
- def _matches_media(target: "Target") -> bool:
+ def _matches_media(target: Target) -> bool:
if not target._settings.media:
return True
else:
- return target._settings.media is target.metadata.media
+ return target._settings.media is target.metadata.to_media_type()
@property
def provider_type(self) -> ProviderType:
- provider_type = self._settings.api_for(self.metadata.media)
+ provider_type = self._settings.api_for(self.metadata.to_media_type())
assert provider_type
return provider_type
@property
- def directory(self) -> Optional[Path]:
- directory = getattr(self._settings, f"{self.metadata.media.value}_directory")
+ def directory(self) -> Path | None:
+ settings_key = f"{self.metadata.to_media_type().value}_directory"
+ directory = getattr(self._settings, settings_key)
return Path(directory) if directory else None
@property
@@ -102,9 +103,7 @@ def destination(self) -> Path:
dir_head = Path(dir_head_)
else:
dir_head = self.source.parent
- file_path = format(
- self.metadata, self._settings.formatting_for(self.metadata.media)
- )
+ file_path = format(self.metadata, self._settings.formatting_for(self.metadata))
dir_tail, filename = path.split(Path(file_path))
filename = filename_replace(filename, self._settings.replace_after)
if self._settings.scene:
@@ -116,8 +115,14 @@ def destination(self) -> Path:
return Path(directory, filename)
def _parse(self, file_path: Path):
- path_data: Dict[str, Any] = {}
- options = {"type": self._settings.media}
+ path_data: dict[str, Any] = {"language": self._settings.language}
+ if is_subtitle(self.source):
+ try:
+ path_data["language"] = Language.parse(self.source.stem[-2:])
+ file_path = Path(self.source.parent, self.source.stem[:-2])
+ except MnamerException:
+ pass
+ options = {"type": self._settings.media, "language": path_data["language"]}
raw_data = dict(guessit(str(file_path), options))
if isinstance(raw_data.get("season"), list):
raw_data = dict(guessit(str(file_path.parts[-1]), options))
@@ -142,7 +147,7 @@ def _parse(self, file_path: Path):
MediaType.MOVIE: MetadataMovie,
None: Metadata,
}[media_type]
- self.metadata = meta_cls(language=self._settings.language)
+ self.metadata = meta_cls()
self.metadata.quality = (
" ".join(
path_data[key]
@@ -159,6 +164,7 @@ def _parse(self, file_path: Path):
)
or None
)
+ self.metadata.language = path_data.get("language")
self.metadata.group = path_data.get("release_group")
self.metadata.container = file_path.suffix or None
if not self.metadata.language:
@@ -170,10 +176,10 @@ def _parse(self, file_path: Path):
self.metadata.language_sub = path_data.get("subtitle_language")
except MnamerException:
pass
- if self.metadata.media is MediaType.MOVIE:
+ if isinstance(self.metadata, MetadataMovie):
self.metadata.name = path_data.get("title")
self.metadata.year = path_data.get("year")
- elif self.metadata.media is MediaType.EPISODE:
+ elif isinstance(self.metadata, MetadataEpisode):
self.metadata.date = path_data.get("date")
self.metadata.episode = path_data.get("episode")
self.metadata.season = path_data.get("season")
@@ -216,7 +222,7 @@ def _replace_before(self) -> None:
value = str_replace(value, self._settings.replace_before)
setattr(self.metadata, attr, value)
- def query(self) -> List[Metadata]:
+ def query(self) -> list[Metadata]:
"""Queries the target's respective media provider for metadata."""
results = self._provider.search(self.metadata)
if not results:
diff --git a/mnamer/tty.py b/mnamer/tty.py
index 29630d2b..979b7fa8 100644
--- a/mnamer/tty.py
+++ b/mnamer/tty.py
@@ -1,7 +1,7 @@
"""Provides an interface for handling user input and printing output."""
import traceback
-from typing import Any, Callable, Dict, List, Optional
+from typing import Any, Callable
from teletype import codes
from teletype.components import ChoiceHelper, SelectOne
@@ -18,16 +18,8 @@
no_style: bool = False
verbose: bool = False
-__all__ = [
- "msg",
- "metadata_prompt",
- "configure",
- "metadata_guess",
- "crash_report",
-]
-
-def _chars() -> Dict[str, str]:
+def _chars() -> dict[str, str]:
if no_style:
chars = codes.CHARS_ASCII
else:
@@ -36,7 +28,9 @@ def _chars() -> Dict[str, str]:
return chars
-def _abort_helpers() -> List[ChoiceHelper]:
+def _abort_helpers() -> tuple[
+ ChoiceHelper[MnamerSkipException], ChoiceHelper[MnamerAbortException]
+]:
if no_style:
style = None
skip_mnemonic = "[s]"
@@ -45,21 +39,21 @@ def _abort_helpers() -> List[ChoiceHelper]:
style = "dark"
skip_mnemonic = "s"
quit_mnemonic = "q"
- return [
- ChoiceHelper(MnamerSkipException, "skip", style, skip_mnemonic),
- ChoiceHelper(MnamerAbortException, "quit", style, quit_mnemonic),
- ]
+ return (
+ ChoiceHelper(MnamerSkipException(), "skip", style, skip_mnemonic),
+ ChoiceHelper(MnamerAbortException(), "quit", style, quit_mnemonic),
+ )
def _msg_format(body: Any):
- converter_map: Dict[type, Callable] = {
+ converter_map: dict[type, Callable] = {
dict: format_dict,
list: format_iter,
tuple: format_iter,
set: format_iter,
MnamerException: format_exception,
}
- converter: Optional[Callable] = converter_map.get(type(body), str)
+ converter: Callable | None = converter_map.get(type(body), str)
if converter:
body = converter(body)
else:
@@ -91,20 +85,17 @@ def error(body: Any):
msg(body, message_type=MessageType.ERROR, debug=False)
-def metadata_prompt(matches: List[Metadata]) -> Optional[Metadata]: # pragma: no cover
+def metadata_prompt(matches: list[Metadata]) -> Metadata: # pragma: no cover
"""Prompts user to choose a match from a list of matches."""
msg("select match")
- options = matches + _abort_helpers() # type: ignore
- selector = SelectOne(options, **_chars())
+ selector = SelectOne([*matches, *_abort_helpers()], **_chars())
choice = selector.prompt()
if isinstance(choice, (MnamerAbortException, MnamerSkipException)):
raise choice
return choice
-def metadata_guess(
- metadata: Metadata,
-) -> Optional[Metadata]: # pragma: no cover
+def metadata_guess(metadata: Metadata) -> Metadata: # pragma: no cover
"""Prompts user to confirm a single match."""
label = str(metadata)
if no_style:
@@ -112,7 +103,7 @@ def metadata_guess(
else:
label += style_format(" (best guess)", "blue")
option = ChoiceHelper(metadata, label)
- selector = SelectOne([option] + _abort_helpers(), **_chars())
+ selector = SelectOne([option, *_abort_helpers()], **_chars())
choice = selector.prompt()
if isinstance(choice, (MnamerAbortException, MnamerSkipException)):
raise choice
@@ -120,10 +111,10 @@ def metadata_guess(
return choice
-def subtitle_prompt() -> Metadata:
+def subtitle_prompt() -> Language:
msg("select language")
choices = [ChoiceHelper(language, language.name) for language in Language.all()]
- selector = SelectOne(choices + _abort_helpers(), **_chars())
+ selector = SelectOne([*choices, *_abort_helpers()], **_chars())
choice = selector.prompt()
if isinstance(choice, (MnamerAbortException, MnamerSkipException)):
raise choice
diff --git a/mnamer/types.py b/mnamer/types.py
index 8d2a251d..9999a4dd 100644
--- a/mnamer/types.py
+++ b/mnamer/types.py
@@ -1,14 +1,18 @@
"""Enum type definitions."""
+from __future__ import annotations
from enum import Enum
-
-__all__ = ["MediaType", "MessageType", "ProviderType", "SettingType"]
+from typing import Type
class MediaType(Enum):
EPISODE = "episode"
MOVIE = "movie"
+ @classmethod
+ def to_media_type(cls) -> Type[MediaType]:
+ return cls
+
class MessageType(Enum):
INFO = None
diff --git a/mnamer/utils.py b/mnamer/utils.py
index ee8457da..84df7870 100644
--- a/mnamer/utils.py
+++ b/mnamer/utils.py
@@ -6,7 +6,7 @@
from os import walk
from os.path import exists, expanduser, expandvars, getsize, splitdrive, splitext
from pathlib import Path, PurePath
-from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
+from typing import Any, Callable, Iterator
from unicodedata import normalize
import requests_cache
@@ -14,41 +14,8 @@
from mnamer.const import CACHE_PATH, CURRENT_YEAR, SUBTITLE_CONTAINERS
-__all__ = [
- "clean_dict",
- "clear_cache",
- "crawl_in",
- "crawl_out",
- "filename_replace",
- "filter_blacklist",
- "filter_containers",
- "findall",
- "fn_chain",
- "fn_pipe",
- "format_dict",
- "format_exception",
- "format_iter",
- "get_filesize",
- "get_session",
- "is_subtitle",
- "json_dumps",
- "json_loads",
- "normalize_container",
- "normalize_containers",
- "parse_date",
- "request_json",
- "str_fix_padding",
- "str_replace",
- "str_replace_slashes",
- "str_sanitize",
- "str_scenify",
- "str_title_case",
- "year_parse",
- "year_range_parse",
-]
-
-
-def clean_dict(target_dict: Dict[Any, Any], whitelist=None) -> Dict[Any, Any]:
+
+def clean_dict(target_dict: dict, whitelist=None) -> dict:
"""Convenience function that removes a dicts keys that have falsy values."""
return {
str(k).strip(): str(v).strip()
@@ -62,7 +29,7 @@ def clear_cache():
get_session().cache.clear()
-def crawl_in(file_paths: List[Path], recurse: bool = False) -> List[Path]:
+def crawl_in(file_paths: list[Path], recurse: bool = False) -> list[Path]:
"""Looks for files amongst or within paths provided."""
found_files = set()
for file_path in file_paths:
@@ -79,7 +46,7 @@ def crawl_in(file_paths: List[Path], recurse: bool = False) -> List[Path]:
return sorted(list(found_files))
-def crawl_out(filename: Union[str, Path, PurePath]) -> Optional[Path]:
+def crawl_out(filename: str | Path | PurePath) -> Path | None:
"""Looks for a file in the home directory and each directory up from cwd."""
working_dir = Path.cwd()
while True:
@@ -94,14 +61,14 @@ def crawl_out(filename: Union[str, Path, PurePath]) -> Optional[Path]:
return target if target.exists() else None
-def filename_replace(filename: str, replacements: Dict[str, str]) -> str:
+def filename_replace(filename: str, replacements: dict[str, str]) -> str:
"""Replaces keys in replacements dict with their values."""
base, container = splitext(filename)
base = str_replace(base, replacements)
return base + container
-def filter_blacklist(paths: List[Path], blacklist: List[str]) -> List[Path]:
+def filter_blacklist(paths: list[Path], blacklist: list[str]) -> list[Path]:
"""Filters (set difference) paths by a collection of regex pattens."""
return [
path.absolute()
@@ -115,8 +82,8 @@ def filter_blacklist(paths: List[Path], blacklist: List[str]) -> List[Path]:
def filter_containers(
- file_paths: List[Path], valid_containers: List[str]
-) -> List[Path]:
+ file_paths: list[Path], valid_containers: list[str]
+) -> list[Path]:
"""Filters (set intersection) a collection of containers."""
valid_containers = normalize_containers(valid_containers)
return [
@@ -150,7 +117,7 @@ def resolver(x):
return resolver
-def format_dict(body: Dict[Any, Any]) -> str:
+def format_dict(body: dict) -> str:
"""
Formats a dictionary into a multi-line bulleted string of key-value pairs.
"""
@@ -168,11 +135,11 @@ def format_iter(body: list) -> str:
return "\n".join(sorted([f" - {getattr(v, 'value', v)}" for v in body]))
-def is_subtitle(container: Optional[str]) -> bool:
+def is_subtitle(container: str | Path | None) -> bool:
"""Returns True if container is a subtitle container."""
if not container:
return False
- return container.endswith(tuple(SUBTITLE_CONTAINERS))
+ return str(container).endswith(tuple(SUBTITLE_CONTAINERS))
def get_session() -> requests_cache.CachedSession:
@@ -209,7 +176,7 @@ def get_filesize(path: Path) -> str:
return f"{size:.{2}f}{unit}"
-def json_dumps(d: Dict[str, Any]) -> str:
+def json_dumps(d: dict[str, Any]) -> str:
"""A wrapper for json.dumps."""
return json.dumps(
{k: getattr(v, "value", v) for k, v in d.items()},
@@ -222,7 +189,7 @@ def json_dumps(d: Dict[str, Any]) -> str:
)
-def json_loads(path: str) -> Dict[str, Any]:
+def json_loads(path: str) -> dict[str, Any]:
json_data = ""
path = expanduser(path)
path = expandvars(path)
@@ -240,12 +207,12 @@ def normalize_container(container: str) -> str:
return container.lower()
-def normalize_containers(container_list: List[str]) -> List[str]:
+def normalize_containers(container_list: list[str]) -> list[str]:
"""For a list of containers ensures that all containers begin with a dot."""
return [normalize_container(container) for container in container_list]
-def parse_date(value: Union[str, dt.date, dt.datetime]) -> dt.date:
+def parse_date(value: str | dt.date | dt.datetime) -> dt.date:
"""Converts an ambiguously formatted date type into a date object."""
if isinstance(value, str):
value = value.replace("/", "-")
@@ -258,11 +225,11 @@ def parse_date(value: Union[str, dt.date, dt.datetime]) -> dt.date:
def request_json(
url,
- parameters: Optional[Union[dict, list]] = None,
- body: Optional[dict] = None,
- headers: Optional[dict] = None,
+ parameters: dict | list | None = None,
+ body: dict | None = None,
+ headers: dict | None = None,
cache: bool = True,
-) -> Tuple[int, dict]:
+) -> tuple[int, dict]:
"""
Queries a url for json data.
@@ -330,7 +297,7 @@ def str_fix_padding(s: str) -> str:
return s if len_before == len_after else str_fix_padding(s)
-def str_replace(s: str, replacements: Dict[str, str]) -> str:
+def str_replace(s: str, replacements: dict[str, str]) -> str:
"""Replaces keys in replacements dict with their values."""
for word, replacement in replacements.items():
if word in s:
@@ -511,7 +478,7 @@ def str_title_case(s: str) -> str:
return s
-def year_parse(s: str) -> Optional[int]:
+def year_parse(s: str) -> int | None:
"""Parses a year from a string."""
regex = r"((?:19|20)\d{2})(?:$|[-/]\d{2}[-/]\d{2})"
try:
@@ -520,9 +487,7 @@ def year_parse(s: str) -> Optional[int]:
return None
-def year_range_parse(
- years: Optional[Union[str, int]], tolerance: int = 1
-) -> Tuple[int, int]:
+def year_range_parse(years: str | int | None, tolerance: int = 1) -> tuple[int, int]:
"""Parses a year or dash-delimited year range."""
regex = r"^((?:19|20)\d{2})?([-,: ]*)?((?:19|20)\d{2})?$"
default_start = 1900
diff --git a/pyproject.toml b/pyproject.toml
index ec8edcc9..7ad6c4dc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ description = "A command-line utility for organizing media files."
authors = [{ name = "Jessy Williams", email = "jessy@jessywilliams.com" }]
maintainers = [{ name = "Jessy Williams", email = "jessy@jessywilliams.com" }]
license = { file = "LICENSE.txt" }
-requires-python = ">=3.8"
+requires-python = ">=3.10"
dynamic = ["version", "readme", "dependencies", "optional-dependencies"]
scripts = { mnamer = "mnamer.__main__:main" }
urls = { repository = "https://github.com/jkwill87/mnamer" }
@@ -16,25 +16,23 @@ packages = ["mnamer"]
"*" = ["LICENSE.txt", "README.md", "py.typed"]
[tool.setuptools.dynamic]
-version = { attr = "mnamer.__version__.VERSION" }
readme = { file = ["README.md"] }
dependencies = { file = "requirements.txt" }
[tool.setuptools.dynamic.optional-dependencies]
dev = { file = "requirements-dev.txt" }
-test = { file = "requirements-test.txt" }
[build-system]
-requires = ["setuptools >= 61.0.0", "wheel"]
+requires = ["setuptools >= 61.0.0", "setuptools_scm[toml] >= 6.2", "wheel"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 88
-target-version = ["py38"]
+target-version = ["py310"]
[tool.pyright]
reportGeneralTypeIssues = "information"
-pythonVersion = "3.8"
+pythonVersion = "3.10"
include = ["mnamer"]
exclude = ["playground.py", "venv"]
venv = ["venv"]
@@ -54,4 +52,13 @@ disable = ["C0103", "C0114", "C0116", "C0209", "E0401", "R0801"]
[tool.mypy]
incremental = false
-python_version = "3.8"
+python_version = "3.10"
+
+[tool.setuptools_scm]
+local_scheme = "dirty-tag"
+write_to = "mnamer/__version__.py"
+write_to_template = '''# file generated by setuptools_scm
+# don't change, don't track in version control
+
+__version__ = "{version}"
+'''
diff --git a/requirements-dev.txt b/requirements-dev.txt
index d625530c..4ff1a930 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,10 +1,14 @@
black ~= 22.10.0
build ~= 0.9.0
+codecov ~= 2.1.0
isort ~= 5.10.0
mypy ~= 0.982
+pyflakes ~= 3.0.1
pylint ~= 2.15.0
+pytest ~= 7.2.0
+pytest-cov ~= 4.0.0
+pytest-rerunfailures ~= 10.2
setuptools ~= 65.6.3
twine ~= 4.0.0
types-requests ~= 2.28
-vbump ~= 1.2.0
wheel ~= 0.38.0
diff --git a/requirements-test.txt b/requirements-test.txt
deleted file mode 100644
index b7c0da6a..00000000
--- a/requirements-test.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-codecov ~= 2.1.0
-pytest-cov ~= 4.0.0
-pytest-rerunfailures ~= 10.2
-pytest ~= 7.2.0
diff --git a/requirements.txt b/requirements.txt
index cafd400c..a7b0e29e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,5 +3,6 @@ babelfish ~= 0.6.0
guessit ~= 3.5.0
requests == 2.*
requests_cache ~= 0.9.7
-setuptools ~= 61.0.0
+setuptools_scm ~= 7.1.0
teletype >= 1.1.0,<1.4.0
+typing-extensions ~= 4.4.0
diff --git a/tests/__init__.py b/tests/__init__.py
index 267d7118..51f5f36e 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,21 +1,10 @@
import datetime as dt
-from typing import Any, Dict, NamedTuple
+from typing import Any, NamedTuple
from mnamer.const import SUBTITLE_CONTAINERS
from mnamer.language import Language
from mnamer.types import ProviderType
-__all__ = [
- "DEFAULT_SETTINGS",
- "EPISODE_META",
- "JUNK_TEXT",
- "MOVIE_META",
- "TEST_DATE",
- "RUSSIAN_LANG",
- "E2EResult",
- "MockRequestResponse",
-]
-
DEFAULT_SETTINGS = {
"batch": False,
"config_dump": False,
@@ -127,7 +116,7 @@ def __init__(self, status: int, content: str) -> None:
self.status_code = status
self.content = content
- def json(self) -> Dict[str, Any]:
+ def json(self) -> dict[str, Any]:
from json import loads
return loads(self.content)
diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py
index 076df464..5cead780 100644
--- a/tests/e2e/conftest.py
+++ b/tests/e2e/conftest.py
@@ -9,7 +9,7 @@
from mnamer.frontends import Cli
from mnamer.setting_store import SettingStore
from mnamer.target import Target
-from tests import *
+from tests import E2EResult
# Move up to root directory if run from subdirectory
cwd = Path().resolve()
diff --git a/tests/e2e/test_directives.py b/tests/e2e/test_directives.py
index 99013ca1..391d23e4 100644
--- a/tests/e2e/test_directives.py
+++ b/tests/e2e/test_directives.py
@@ -2,9 +2,12 @@
import pytest
-from mnamer.__version__ import VERSION
+from mnamer.const import VERSION
-pytestmark = pytest.mark.e2e
+pytestmark = [
+ pytest.mark.e2e,
+ pytest.mark.flaky(reruns=1),
+]
@pytest.mark.parametrize("flag", ("-V", "--version"))
@@ -22,21 +25,6 @@ def test_directives__clear_cache(mock_clear_cache: MagicMock, e2e_run):
mock_clear_cache.assert_called_once()
-# @pytest.mark.parametrize("key", SettingStore._serializable_fields())
-# @patch("mnamer.utils.crawl_out")
-# def test_directives__config_dump(mock_crawl_out: MagicMock, key: str, e2e_run):
-# mock_crawl_out.return_value = None
-# result = e2e_run("--config_dump")
-# assert result.code == 0
-# if key.startswith("api_key"):
-# return
-# json_out = json.loads(result.out)
-# value = DEFAULT_SETTINGS[key]
-# expected = getattr(value, "value", value)
-# actual = json_out[key]
-# assert actual == expected
-
-
@pytest.mark.omdb
@pytest.mark.usefixtures("setup_test_dir")
def test_id__omdb(e2e_run, setup_test_files):
diff --git a/tests/e2e/test_errors.py b/tests/e2e/test_errors.py
index a3ef0944..d3d739ce 100644
--- a/tests/e2e/test_errors.py
+++ b/tests/e2e/test_errors.py
@@ -1,9 +1,12 @@
import pytest
from mnamer.argument import ArgLoader
-from tests import *
+from tests import JUNK_TEXT
-pytestmark = pytest.mark.e2e
+pytestmark = [
+ pytest.mark.e2e,
+ pytest.mark.flaky(reruns=1),
+]
def test_invalid_arguments(e2e_run):
diff --git a/tests/e2e/test_moving.py b/tests/e2e/test_moving.py
index 51c3658f..edf004af 100644
--- a/tests/e2e/test_moving.py
+++ b/tests/e2e/test_moving.py
@@ -4,7 +4,10 @@
from mnamer.const import SUBTITLE_CONTAINERS
-pytestmark = pytest.mark.e2e
+pytestmark = [
+ pytest.mark.e2e,
+ pytest.mark.flaky(reruns=2),
+]
@pytest.mark.usefixtures("setup_test_dir")
diff --git a/tests/local/test_argument.py b/tests/local/test_argument.py
index d0be69db..13dd4f91 100644
--- a/tests/local/test_argument.py
+++ b/tests/local/test_argument.py
@@ -78,21 +78,27 @@ def test_arg_loader__format_help():
def test_arg_parser__load__valid_parameter():
- spec = SettingSpec(group=SettingType.PARAMETER, flags=["-f"], help="foo", type=int)
+ spec = SettingSpec(
+ group=SettingType.PARAMETER, flags=["-f"], help="foo", typevar=int
+ )
arg_parser = ArgLoader(spec)
with patch.object(sys, "argv", ["mnamer", "-f", "01"]):
assert arg_parser.load() == {"f": 1}
def test_arg_parser__load__valid_directive():
- spec = SettingSpec(group=SettingType.DIRECTIVE, flags=["-f"], help="foo", type=int)
+ spec = SettingSpec(
+ group=SettingType.DIRECTIVE, flags=["-f"], help="foo", typevar=int
+ )
arg_parser = ArgLoader(spec)
with patch.object(sys, "argv", ["mnamer", "-f", "01"]):
assert arg_parser.load() == {"f": 1}
def test_arg_parser__load__valid_positional():
- spec = SettingSpec(group=SettingType.POSITIONAL, flags=["f"], help="foo", type=int)
+ spec = SettingSpec(
+ group=SettingType.POSITIONAL, flags=["f"], help="foo", typevar=int
+ )
arg_parser = ArgLoader(spec)
with patch.object(sys, "argv", ["mnamer", "01"]):
assert arg_parser.load() == {"f": 1}
diff --git a/tests/local/test_metadata.py b/tests/local/test_metadata.py
index 8fe1bbed..eda54705 100644
--- a/tests/local/test_metadata.py
+++ b/tests/local/test_metadata.py
@@ -2,19 +2,13 @@
import pytest
-from mnamer.metadata import *
-from mnamer.types import MediaType
+from mnamer.metadata import Metadata, MetadataEpisode, MetadataMovie
pytestmark = pytest.mark.local
TEXT_CASES = ["test", "Test", "TEST", "TeSt"]
-def test_metadata__convert__media():
- metadata = Metadata(media="episode")
- assert metadata.media is MediaType.EPISODE
-
-
@pytest.mark.parametrize("value", TEXT_CASES)
def test_metadata__convert_synopsis(value):
metadata = Metadata(synopsis=value)
diff --git a/tests/local/test_setting_spec.py b/tests/local/test_setting_spec.py
index 45e21ccb..f19b010b 100644
--- a/tests/local/test_setting_spec.py
+++ b/tests/local/test_setting_spec.py
@@ -15,7 +15,7 @@ def test_setting_spec__serialize__default():
"group": SettingType.PARAMETER,
"help": None,
"nargs": None,
- "type": None,
+ "typevar": None,
}
setting_spec = SettingSpec(SettingType.PARAMETER)
assert setting_spec.as_dict() == default
@@ -30,7 +30,7 @@ def test_setting_spec__serialize__override():
"group": SettingType.PARAMETER,
"help": "foos your bars",
"nargs": "+",
- "type": int,
+ "typevar": int,
}
setting_spec = SettingSpec(**spec)
assert setting_spec.as_dict() == spec
diff --git a/tests/local/test_setting_store.py b/tests/local/test_setting_store.py
index 085b1bd3..f0605913 100644
--- a/tests/local/test_setting_store.py
+++ b/tests/local/test_setting_store.py
@@ -2,7 +2,7 @@
from mnamer.setting_store import SettingStore
from mnamer.types import MediaType, ProviderType
-from tests import *
+from tests import DEFAULT_SETTINGS
pytestmark = pytest.mark.local
diff --git a/tests/local/test_target.py b/tests/local/test_target.py
index 64fd71b1..1eaca9ec 100644
--- a/tests/local/test_target.py
+++ b/tests/local/test_target.py
@@ -3,8 +3,9 @@
import pytest
+from mnamer.metadata import MetadataEpisode, MetadataMovie
from mnamer.setting_store import SettingStore
-from mnamer.target import *
+from mnamer.target import Target
from mnamer.types import MediaType
pytestmark = pytest.mark.local
@@ -12,12 +13,12 @@
def test_parse__media__movie():
target = Target(Path("ninja turtles (1990).mkv"), SettingStore())
- assert target.metadata.media is MediaType.MOVIE
+ assert target.metadata.to_media_type() is MediaType.MOVIE
def test_parse__media__episode():
target = Target(Path("ninja turtles s01e01.mkv"), SettingStore())
- assert target.metadata.media is MediaType.EPISODE
+ assert target.metadata.to_media_type() is MediaType.EPISODE
def test_parse__quality():
@@ -41,43 +42,49 @@ def test_parse__container():
def test_parse__date():
file_path = Path("the.colbert.show.2010.10.01.avi")
target = Target(file_path, SettingStore())
+ assert isinstance(target.metadata, MetadataEpisode)
assert target.metadata.date == dt.date(2010, 10, 1)
def test_parse__episode():
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
target = Target(file_path, SettingStore())
+ assert isinstance(target.metadata, MetadataEpisode)
assert target.metadata.episode == 4
def test_parse__season():
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
target = Target(file_path, SettingStore())
+ assert isinstance(target.metadata, MetadataEpisode)
assert target.metadata.season == 1
def test_parse__series():
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
target = Target(file_path, SettingStore())
+ assert isinstance(target.metadata, MetadataEpisode)
assert target.metadata.series == "Ninja Turtles"
def test_parse__year():
file_path = Path("the.goonies.1985")
target = Target(file_path, SettingStore())
+ assert isinstance(target.metadata, MetadataMovie)
assert target.metadata.year == 1985
def testparse__name():
file_path = Path("the.goonies.1985")
target = Target(file_path, SettingStore())
+ assert isinstance(target.metadata, MetadataMovie)
assert target.metadata.name == "The Goonies"
@pytest.mark.parametrize("media", MediaType)
def test_media__override(media: MediaType):
target = Target(Path(), SettingStore(media=media))
- assert target.metadata.media is media
+ assert target.metadata.to_media_type() == media
def test_directory__movie():
@@ -105,7 +112,7 @@ def test_ambiguous_subtitle_language():
def test_destination__simple():
- target = Target(Path("star.trek.enterprise.s01e1.mkv"))
+ pass # TODO
def test_query():
diff --git a/tests/local/test_tty.py b/tests/local/test_tty.py
index 45b5e64d..69037654 100644
--- a/tests/local/test_tty.py
+++ b/tests/local/test_tty.py
@@ -45,10 +45,10 @@ def test_abort_helpers():
helpers = tty._abort_helpers()
assert len(helpers) == 2
assert helpers[0].label == "skip"
- assert helpers[0].value == MnamerSkipException
+ assert isinstance(helpers[0].value, MnamerSkipException)
assert helpers[0]._bracketed is False
assert helpers[1].label == "quit"
- assert helpers[1].value == MnamerAbortException
+ assert isinstance(helpers[1].value, MnamerAbortException)
assert helpers[1]._bracketed is False
@@ -58,8 +58,8 @@ def test_abort_helpers__no_style():
helpers = tty._abort_helpers()
assert len(helpers) == 2
assert helpers[0].label == "skip"
- assert helpers[0].value == MnamerSkipException
+ assert isinstance(helpers[0].value, MnamerSkipException)
assert helpers[0]._bracketed is True
assert helpers[1].label == "quit"
- assert helpers[1].value == MnamerAbortException
+ assert isinstance(helpers[1].value, MnamerAbortException)
assert helpers[1]._bracketed is True
diff --git a/tests/local/test_utils.py b/tests/local/test_utils.py
index c576578a..0a8fe09b 100644
--- a/tests/local/test_utils.py
+++ b/tests/local/test_utils.py
@@ -1,5 +1,4 @@
from pathlib import Path
-from typing import Dict, List
from unittest.mock import patch
import pytest
@@ -7,12 +6,34 @@
from mnamer.const import CURRENT_YEAR, SUBTITLE_CONTAINERS
from mnamer.types import MediaType
-from mnamer.utils import *
-from tests import *
+from mnamer.utils import (
+ clean_dict,
+ crawl_in,
+ crawl_out,
+ filter_blacklist,
+ filter_containers,
+ fn_chain,
+ fn_pipe,
+ format_dict,
+ format_exception,
+ format_iter,
+ is_subtitle,
+ normalize_container,
+ parse_date,
+ request_json,
+ str_fix_padding,
+ str_replace,
+ str_sanitize,
+ str_scenify,
+ str_title_case,
+ year_parse,
+ year_range_parse,
+)
+from tests import JUNK_TEXT, MockRequestResponse
pytestmark = pytest.mark.local
-TEST_FILES: Dict[str, Path] = {
+TEST_FILES: dict[str, Path] = {
test_file: Path(*test_file.split("/"))
for test_file in (
"Avengers Infinity War/Avengers.Infinity.War.srt",
@@ -351,7 +372,7 @@ def test_filter_containers__filter_none():
@pytest.mark.parametrize("containers", (["jpg"], [".jpg"]))
def test_filter_containers__filter_multiple_paths_single_pattern(
- containers: List[str],
+ containers: list[str],
):
expected = paths_for("Images/Photos/DCM0001.jpg", "Images/Photos/DCM0002.jpg")
actual = filter_containers(FILTER_FILENAMES, containers)
@@ -360,7 +381,7 @@ def test_filter_containers__filter_multiple_paths_single_pattern(
@pytest.mark.parametrize("containers", (["mkv", "zip"], [".mkv", ".zip"]))
def test_filter_containers__filter_multiple_paths_multi_pattern(
- containers: List[str],
+ containers: list[str],
):
expected = paths_for(
"Desktop/temp.zip",
@@ -378,7 +399,7 @@ def test_filter_containers__filter_multiple_paths_multi_pattern(
@pytest.mark.parametrize("containers", (["mp4", "zip"], [".mp4", ".zip"]))
def test_filter_containers__filter_single_path_multi_pattern(
- containers: List[str],
+ containers: list[str],
):
filepaths = paths_for("Images/Skiing Trip.mp4")
expected = filepaths
diff --git a/tests/network/test_endpoints__omdb.py b/tests/network/test_endpoints__omdb.py
index 9b64858c..605312ad 100644
--- a/tests/network/test_endpoints__omdb.py
+++ b/tests/network/test_endpoints__omdb.py
@@ -5,7 +5,7 @@
from mnamer.endpoints import omdb_search, omdb_title
from mnamer.exceptions import MnamerException, MnamerNotFoundException
from mnamer.providers import Omdb
-from tests import *
+from tests import JUNK_TEXT, MockRequestResponse
pytestmark = [
pytest.mark.network,
diff --git a/tests/network/test_endpoints__tmdb.py b/tests/network/test_endpoints__tmdb.py
index 697e39c2..55e2deab 100644
--- a/tests/network/test_endpoints__tmdb.py
+++ b/tests/network/test_endpoints__tmdb.py
@@ -3,7 +3,7 @@
from mnamer.endpoints import tmdb_find, tmdb_movies, tmdb_search_movies
from mnamer.exceptions import MnamerException, MnamerNotFoundException
from mnamer.providers import Tmdb
-from tests import *
+from tests import JUNK_TEXT, RUSSIAN_LANG
pytestmark = [
pytest.mark.network,
diff --git a/tests/network/test_endpoints__tvdb.py b/tests/network/test_endpoints__tvdb.py
index ce738447..87d5c905 100644
--- a/tests/network/test_endpoints__tvdb.py
+++ b/tests/network/test_endpoints__tvdb.py
@@ -12,7 +12,7 @@
from mnamer.exceptions import MnamerException, MnamerNotFoundException
from mnamer.language import Language
from mnamer.providers import Tvdb
-from tests import *
+from tests import JUNK_TEXT, RUSSIAN_LANG
pytestmark = [
pytest.mark.network,
diff --git a/tests/network/test_endpoints__tvmaze.py b/tests/network/test_endpoints__tvmaze.py
index c0ce5937..f5ace10e 100644
--- a/tests/network/test_endpoints__tvmaze.py
+++ b/tests/network/test_endpoints__tvmaze.py
@@ -10,7 +10,7 @@
tvmaze_show_single_search,
)
from mnamer.exceptions import MnamerException, MnamerNotFoundException
-from tests import *
+from tests import EPISODE_META, JUNK_TEXT, TEST_DATE
pytestmark = [
pytest.mark.network,
diff --git a/tests/network/test_providers__omdb.py b/tests/network/test_providers__omdb.py
index f06d4227..5cc9b97f 100644
--- a/tests/network/test_providers__omdb.py
+++ b/tests/network/test_providers__omdb.py
@@ -1,11 +1,9 @@
-from typing import Dict
-
import pytest
from mnamer.exceptions import MnamerNotFoundException
from mnamer.metadata import MetadataMovie
from mnamer.providers import Omdb
-from tests import *
+from tests import JUNK_TEXT, MOVIE_META
pytestmark = [
pytest.mark.network,
@@ -20,7 +18,7 @@ def provider():
@pytest.mark.parametrize("meta", MOVIE_META.values(), ids=list(MOVIE_META))
-def test_search__id(meta: Dict[str, str], provider: Omdb):
+def test_search__id(meta: dict[str, str], provider: Omdb):
query = MetadataMovie(id_imdb=meta["id_imdb"])
results = list(provider.search(query))
assert len(results) == 1
diff --git a/tests/network/test_providers__tmdb.py b/tests/network/test_providers__tmdb.py
index 7171d4de..ae615ed5 100644
--- a/tests/network/test_providers__tmdb.py
+++ b/tests/network/test_providers__tmdb.py
@@ -3,7 +3,7 @@
from mnamer.exceptions import MnamerNotFoundException
from mnamer.metadata import MetadataMovie
from mnamer.providers import Tmdb
-from tests import *
+from tests import JUNK_TEXT, MOVIE_META
pytestmark = [
pytest.mark.network,
diff --git a/tests/network/test_providers__tvdb.py b/tests/network/test_providers__tvdb.py
index 06b8066f..14f1fb8c 100644
--- a/tests/network/test_providers__tvdb.py
+++ b/tests/network/test_providers__tvdb.py
@@ -5,7 +5,7 @@
from mnamer.exceptions import MnamerNotFoundException
from mnamer.metadata import MetadataEpisode
from mnamer.providers import Tvdb
-from tests import *
+from tests import EPISODE_META, JUNK_TEXT
pytestmark = [
pytest.mark.network,
diff --git a/tests/network/test_providers__tvmaze.py b/tests/network/test_providers__tvmaze.py
index efd96c75..9e00a227 100644
--- a/tests/network/test_providers__tvmaze.py
+++ b/tests/network/test_providers__tvmaze.py
@@ -3,7 +3,7 @@
from mnamer.exceptions import MnamerNotFoundException
from mnamer.metadata import MetadataEpisode
from mnamer.providers import TvMaze
-from tests import *
+from tests import EPISODE_META, JUNK_TEXT, TEST_DATE
pytestmark = [
pytest.mark.network,