From 95a5df00ac0b66f1a4a3990d4dd173ff0d925655 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 11:46:17 +0000 Subject: [PATCH 01/18] [#67] Moved all source files to the `src` folder. --- {timecode => src/timecode}/__init__.py | 0 {timecode => src/timecode}/py.typed | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {timecode => src/timecode}/__init__.py (100%) rename {timecode => src/timecode}/py.typed (100%) diff --git a/timecode/__init__.py b/src/timecode/__init__.py similarity index 100% rename from timecode/__init__.py rename to src/timecode/__init__.py diff --git a/timecode/py.typed b/src/timecode/py.typed similarity index 100% rename from timecode/py.typed rename to src/timecode/py.typed From 12448f22f840f7c5e47c4a6ea59e41d6df24b867 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 11:46:31 +0000 Subject: [PATCH 02/18] [#67] Updated `.gitignore` file. --- .gitignore | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 5d00ebc..698ac12 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,18 @@ -*~* -*.pyc -.DS_Store .coverage +.DS_Store +.pytest_cache/* +.ruff_cache/* +.venv/* +*.egg-info *.egg-info/* +*.pyc *.swp -.venv/* +*~* +build/* +dist/ +docs/doctrees/* docs/html/* docs/latex/* -docs/doctrees/* docs/source/generated/* -dist/ -build/* include/* +__pycache__ \ No newline at end of file From 79a32fa2bf6a5b28d9ce72096044b9e28e065e30 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 11:48:05 +0000 Subject: [PATCH 03/18] [#67] Moved all the content of the `timecode/__init__.py` to `timecode/timecode.py` to leave the `__init__.py` empty. --- src/timecode/{__init__.py => timecode.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/timecode/{__init__.py => timecode.py} (100%) diff --git a/src/timecode/__init__.py b/src/timecode/timecode.py similarity index 100% rename from src/timecode/__init__.py rename to src/timecode/timecode.py From 162d7267116d8f02b6c3e43a93a2fb7bb98975ae Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 11:48:41 +0000 Subject: [PATCH 04/18] [#67] Added a new empty `__init__.py` file. --- src/timecode/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/timecode/__init__.py diff --git a/src/timecode/__init__.py b/src/timecode/__init__.py new file mode 100644 index 0000000..e69de29 From d79b7d15c016d1f9dadb7a52312167c15f9ede08 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 12:56:37 +0000 Subject: [PATCH 05/18] [#67] Added `VERSION` file to easily update library version. --- VERSION | 1 + src/timecode/VERSION | 1 + src/timecode/__init__.py | 1 + src/timecode/_version.py | 12 ++++++++++++ 4 files changed, 15 insertions(+) create mode 120000 VERSION create mode 100644 src/timecode/VERSION create mode 100644 src/timecode/_version.py diff --git a/VERSION b/VERSION new file mode 120000 index 0000000..c941707 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +src/timecode/VERSION \ No newline at end of file diff --git a/src/timecode/VERSION b/src/timecode/VERSION new file mode 100644 index 0000000..8e03717 --- /dev/null +++ b/src/timecode/VERSION @@ -0,0 +1 @@ +1.5.1 \ No newline at end of file diff --git a/src/timecode/__init__.py b/src/timecode/__init__.py index e69de29..e010457 100644 --- a/src/timecode/__init__.py +++ b/src/timecode/__init__.py @@ -0,0 +1 @@ +from timecode._version import __version__ \ No newline at end of file diff --git a/src/timecode/_version.py b/src/timecode/_version.py new file mode 100644 index 0000000..21aad4a --- /dev/null +++ b/src/timecode/_version.py @@ -0,0 +1,12 @@ +"""Parse version.""" + +import os + +# globals +_VERSION = None +_VERSION_FILE = os.path.join(os.path.dirname(__file__), "VERSION") +if os.path.isfile(_VERSION_FILE): + with open(_VERSION_FILE, "r") as _version_file: + _VERSION = _version_file.read().strip() +__version__ = _VERSION or "0.0.0" +"""str: The version of the package.""" From d948c9f68d26aecb13b39fc8458ee2c738157386 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 12:57:42 +0000 Subject: [PATCH 06/18] [#67] Added `Makefile` to start using the Makefile Workflow. --- Makefile | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0f16f06 --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +SHELL:=bash +NUM_CPUS = $(shell nproc || grep -c '^processor' /proc/cpuinfo) +PACKAGE_NAME = timecode +SETUP_PY_FLAGS = --use-distutils +VERSION := $(shell cat VERSION) +VERSION_FILE=$(CURDIR)/VERSION +VIRTUALENV_DIR:=.venv +SYSTEM_PYTHON?=python3 + +all: build FORCE + +.PHONY: help +help: + @echo "" + @echo "Available targets:" + @make -qp | grep -o '^[a-z0-9-]\+' | sort + +.PHONY: venv +venv: + @printf "\n\033[36m--- $@: Creating Local virtualenv '$(VIRTUALENV_DIR)' using '`which python`' ---\033[0m\n" + $(SYSTEM_PYTHON) -m venv $(VIRTUALENV_DIR) + +build: + @printf "\n\033[36m--- $@: Building $(PACKAGE_NAME) ---\033[0m" + @printf "\n\033[36m--- $@: Local install into virtualenv '$(VIRTUALENV_DIR)' ---\033[0m"; + @source ./$(VIRTUALENV_DIR)/bin/activate; \ + printf "\n\033[36m--- $@: Using python interpreter '`which python`' ---\033[0m\n"; \ + pip install uv; \ + uv pip install -r requirements.txt -r requirements-dev.txt; \ + uv build; + +install: + @printf "\n\033[36m--- $@: Installing $(PACKAGE_NAME) to virtualenv at '$(VIRTUALENV_DIR)' using '`which python`' ---\033[0m\n" + source ./$(VIRTUALENV_DIR)/bin/activate; \ + uv pip install ./dist/$(PACKAGE_NAME)-$(VERSION)-*.whl --force-reinstall; + +clean: FORCE + @printf "\n\033[36m--- $@: Clean ---\033[0m\n" + -rm -rf .pytest_cache + -rm -rf .coverage + -rm -rf .ruff_cache + -rm -rf .tox + -rm -rf $(VIRTUALENV_DIR) + -rm -rf dist + -rm -rf build + +clean-all: clean + @printf "\n\033[36m--- $@: Clean All---\033[0m\n" + -rm -f INSTALLED_FILES + -rm -f setuptools-*.egg + -rm -f use-distutils + -rm -f main.py + -rm -Rf htmlcov + -rm .coverage.* + -rm MANIFEST.in + -rm -Rf $(PACKAGE_NAME).egg-info + -rm -Rf $(VIRTUALENV_DIR) + +html: + ./setup.py readme + +new-release: + @printf "\n\033[36m--- $@: Generating New Release ---\033[0m\n" + git add $(VERSION_FILE) + git commit -m "Version $(VERSION)" + git push + git checkout main + git pull + git merge develop + git tag $(VERSION) + git push origin main --tags + @source ./$(VIRTUALENV_DIR)/bin/activate; \ + printf "\n\033[36m--- $@: Using python interpreter '`which python`' ---\033[0m\n"; \ + uv pip install -r requirements.txt; \ + uv pip install -r requirements-dev.txt; \ + uv build; \ + twine check dist/$(PACKAGE_NAME)-$(VERSION).tar.gz; \ + twine upload dist/$(PACKAGE_NAME)-$(VERSION).tar.gz; + +.PHONY: tests +tests: + @printf "\n\033[36m--- $@: Run Tests ---\033[0m" + @printf "\n\033[36m--- $@: Using virtualenv at '$(VIRTUALENV_DIR)' ---\033[0m"; \ + source ./$(VIRTUALENV_DIR)/bin/activate; \ + printf "\n\033[36m--- $@: Using python interpreter '`which python`' ---\033[0m\n"; \ + PYTHONPATH=src pytest -n auto -W ignore --color=yes --cov-report term --cov-report html --cov=src/$(PACKAGE_NAME); + +# https://www.gnu.org/software/make/manual/html_node/Force-Targets.html +FORCE: From e819599e1ad0607a5424f6a936f7e530af3f6e41 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 12:58:53 +0000 Subject: [PATCH 07/18] [#67] Updated `MANIFEST.in` file to include only the required files in the source distribution. --- MANIFEST.in | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index ce75975..e5c0c63 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,14 +1,6 @@ -include *.txt *.ini *.cfg *.rst *.py - -include CHANGELOG -include TODO -include README -include INSTALL -include MANIFEST.in +include CHANGELOG.rst include LICENSE -include timecode.py -recursive-include docs * -recursive-include tests * - -prune docs/build -prune docs/source/generated +include MANIFEST.in +include README.md +global-exclude __pycache__ +global-exclude *.py[cod] \ No newline at end of file From 0cbd0508f10558e590f50a2002ae96bba559728d Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 12:59:27 +0000 Subject: [PATCH 08/18] [#67] Added `pyproject.toml` and removed `setup.cfg` file. --- pyproject.toml | 204 +++++++++++++++++++++++++++++++++++++++++++ requirements-dev.txt | 5 +- requirements.txt | 0 setup.cfg | 30 ------- setup.py | 69 +-------------- 5 files changed, 209 insertions(+), 99 deletions(-) create mode 100644 pyproject.toml create mode 100644 requirements.txt delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..12fa048 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,204 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools", +] + +[project] +authors = [ + {name = "Erkan Özgür Yılmaz", email = "eoyilmaz@gmail.com"}, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: MacOS X", + "Environment :: Win32 (MS Windows)", + "Environment :: X11 Applications", + "Intended Audience :: End Users/Desktop", + "Operating System :: OS Independent", + "Programming Language :: Python", + "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 :: 3.14", + "Topic :: Software Development :: Libraries :: Python Modules", +] +description = "SMPTE Time Code Manipulation Library" +dynamic = ["version"] +keywords = ["video", "timecode", "smpte"] +license = "MIT" +maintainers = [ + {name = "Erkan Özgür Yılmaz", email = "eoyilmaz@gmail.com"}, +] +name = "timecode" +readme = "README.md" +requires-python = ">= 3.9" + +[project.urls] +"Home Page" = "https://github.com/eoyilmaz/timecode" +GitHub = "https://github.com/eoyilmaz/timecode" +Repository = "https://github.com/eoyilmaz/timecode.git" +Download = "https://github.com/eoyilmaz/timecode/releases/" + +[tool.distutils.bdist_rpm] +doc-files = "LICENSE README.md" +fix-python = 1 +packager = "Erkan Ozgur Yilmaz " +release = 1 + +[tool.distutils.bdist_wheel] +universal = 0 + +[tool.distutils.install] +record = "INSTALLED_FILES" + +[tool.setuptools] +include-package-data = false + +[tool.setuptools.packages.find] +where = ["./src"] + +[tool.setuptools.package-data] +timecode = [ + "LICENSE", + "src/timecode/py.typed", + "README.md", + "VERSION", +] + +[tool.setuptools.exclude-package-data] +DisplayCAL = [ + "docs", + "tests", + "tests/__pycache__" +] + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.txt"] } +optional-dependencies.test = { file = ["requirements-dev.txt"] } +version = { file = ["VERSION"] } + +[tool.black] +line-length = 88 + +[tool.flake8] +exclude = [ + "__pycache__", + ".coverage", + ".DS_Store", + ".github", + ".pytest_cache", + ".ruff_cache", + ".venv", + ".vscode", + "build", + "dist", + "docs", + "INSTALLED_FILES", + "MANIFEST.in", + "tests", + "timecode.egg-info", + "VERSION", +] +extend-select = ["B950"] +ignore = ["D107", "E203", "E501", "E701", "SC200", "W503"] +max-complexity = 12 +max-line-length = 80 +per-file-ignores = [ + "DisplayCAL/lib/agw/fmresources.py: B950" +] + +[tool.ruff.lint] +select = [ + "A", # flake8-builtins + "ANN", # flake-annotations + "ASYNC", # flake-async + "B", # flake8-bugbear + "C", # McCabe complexity + "C4", # flake8-comprehensions + "COM", # flake8-commas + "D", # pydocstyle + "E", # pycodestyle + "F", # Pyflakes + "FA", # flake8-future-annotations + "FLY", # flynt + "I", # isort + "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "N", # PEP8 Naming + "NPY", # NumPy-specific rules + "PERF", # perflint + "PIE", # flake8-pie + "PLR", # Pylint - Refactor + "PYI", # flake8-pyi + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific-rules + "Q", # flake8-quotes + "S", # flake-bandit + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "T10", # flake8-debugger + "TC", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "YTT", # flake-2020 + +] +extend-ignore = [ + "ANN002", # missing type annotations args + "ANN003", # missing type annotations kwargs + "COM812", # flake8-commas + "D104", # Missing docstring in in publish package + "D107", # skip docstring for __init__ + "D203", # Incorrect blank line before class + "D209", # New line after last paragraph in docstring + "D213", # Multiline docstring summary second line + "D412", # Bland line between header and content + "D416", # Missing section name colon + "E741", # Ambiguous variable name + "E402", # Module level import not at top of file + "PLR0911", # Too many return statements + "PLR0912", # Too many branches + "PLR0913", # Too many arguments + "PLR0914", # Too many local variables + "PLR0915", # Too many statements + "PLR2004", # Magic value comparison + "PERF203", # Try..except in loop, TODO: Re-enable this later on... + "RUF100", # Unused noqa + "S105", # Hardcoded password + "S110", # try..except..pass + "S603", # Subprocess without shell=True +] + +[tool.ruff.lint.mccabe] +max-complexity = 12 + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.pytest.ini_options] +pythonpath = [ + ".", +] + +[tool.tox] +requires = ["tox>=4.23.2"] +env_list = ["3.9", "3.10", "3.11", "3.12", "3.13"] + +[tool.tox.env_run_base] +description = "run the tests with pytest" +package = "wheel" +wheel_build_env = ".pkg" +deps = [ + "pytest>=6", + "pytest-cov", + "pytest-xdist", +] +commands = [ + ["pytest"], +] + +[tool.mypy] diff --git a/requirements-dev.txt b/requirements-dev.txt index fe83f27..8c40ba9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ build -mock; python_version == '2.7' pytest pytest-cov pytest-xdist -twine \ No newline at end of file +ruff +twine +uv \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 1896b5f..0000000 --- a/setup.cfg +++ /dev/null @@ -1,30 +0,0 @@ -[nosetests] -match=^test -nocapture=1 -cover-package=timecode -with-coverage=1 -cover-erase=1 - -[compile_catalog] -directory = timecode/locale -domain = timecode -statistics = true - -[extract_messages] -add_comments = TRANSLATORS: -output_file = timecode/locale/timecode.pot -width = 80 - -[init_catalog] -domain = timecode -input_file = timecode/locale/timecode.pot -output_dir = timecode/locale - -[update_catalog] -domain = timecode -input_file = timecode/locale/timecode.pot -output_dir = timecode/locale -previous = true - -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py index 66655f9..8bf1ba9 100644 --- a/setup.py +++ b/setup.py @@ -1,67 +1,2 @@ -#!-*- coding: utf-8 -*- - -import re -import os -from setuptools import setup, find_packages - - -def read_file(file_path): - """Read the given file at file_path. - - Args: - file_path (str): The file path to read. - - Returns: - str: The file content. - """ - with open(file_path, encoding="utf-8") as f: - data = f.read() - return data - - -here = os.path.abspath(os.path.dirname(__file__)) -README = read_file(os.path.join(here, 'README.rst')) -CHANGES = read_file(os.path.join(here, 'CHANGELOG.rst')) -METADATA = read_file(os.path.join(here, "timecode", "__init__.py")) - - -def get_meta(meta): - """Return meta. - - Args: - meta (str): The meta to read. i.e. ``version``, ``author`` etc. - """ - meta_match = re.search( - r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), - METADATA, re.M - ) - if meta_match: - meta_value = meta_match.group(1) - return meta_value - - -setup( - name=get_meta("name"), - version=get_meta("version"), - description=get_meta("description"), - long_description='%s\n\n%s' % (README, CHANGES), - long_description_content_type='text/x-rst', - classifiers=[ - "Programming Language :: Python", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Development Status :: 5 - Production/Stable", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - author=get_meta("author"), - author_email=get_meta("author_email"), - url=get_meta("url"), - keywords=['video', 'timecode', 'smpte'], - packages=find_packages(), - include_package_data=True, - package_data={ - "timecode": ["py.typed"], - }, - python_requires=">=3.7", - zip_safe=True, -) +from setuptools import setup +setup() From e8fc0b8f5f457a69a8a47744785cbce8196c49ec Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 13:00:21 +0000 Subject: [PATCH 09/18] [#67] Removed metadata enum values from the `timecode` module. --- src/timecode/timecode.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/timecode/timecode.py b/src/timecode/timecode.py index abcd9b6..6e69872 100644 --- a/src/timecode/timecode.py +++ b/src/timecode/timecode.py @@ -21,13 +21,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__name__ = "timecode" -__version__ = "1.4.1" -__description__ = "SMPTE Time Code Manipulation Library" -__author__ = "Erkan Ozgur Yilmaz" -__author_email__ = "eoyilmaz@gmail.com" -__url__ = "https://github.com/eoyilmaz/timecode" - from fractions import Fraction import sys From 8c1ed361d88a964613fc696878095447dafc9a84 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 13:00:59 +0000 Subject: [PATCH 10/18] [#67] Updated the test file to import the `timecode` module correctly. --- tests/test_timecode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_timecode.py b/tests/test_timecode.py index f6ccbfe..46299f6 100644 --- a/tests/test_timecode.py +++ b/tests/test_timecode.py @@ -1,7 +1,7 @@ #!-*- coding: utf-8 -*- import pytest -from timecode import Timecode, TimecodeError +from timecode.timecode import Timecode, TimecodeError @pytest.mark.parametrize( From 9d2de09c32d8f5465013cd3a1b130f04104bab54 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 13:01:49 +0000 Subject: [PATCH 11/18] [#67] Removed the `upload_to_pypi[.cmd]` files. --- upload_to_pypi | 3 --- upload_to_pypi.cmd | 4 ---- 2 files changed, 7 deletions(-) delete mode 100755 upload_to_pypi delete mode 100644 upload_to_pypi.cmd diff --git a/upload_to_pypi b/upload_to_pypi deleted file mode 100755 index 09458de..0000000 --- a/upload_to_pypi +++ /dev/null @@ -1,3 +0,0 @@ -python -m build -twine check dist/timecode-*.tar.gz -twine upload dist/timecode-*.tar.gz diff --git a/upload_to_pypi.cmd b/upload_to_pypi.cmd deleted file mode 100644 index afdd4cf..0000000 --- a/upload_to_pypi.cmd +++ /dev/null @@ -1,4 +0,0 @@ -del *.pyc /S -..\Scripts\python setup.py clean --all -# ..\Scripts\python setup.py sdist upload -..\Scripts\python setup.py sdist bdist_wheel upload From 39ff783b55f5d59e0dc29a356dde6dfc9db0ae75 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 13:19:11 +0000 Subject: [PATCH 12/18] [#67] Updated the `clean` target in the `Makefile` so that it deletes the `__pycache__` folders. Also updated `clean-all` target to not to delete the `MANIFEST.in` file. --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 0f16f06..fc2163a 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,8 @@ clean: FORCE -rm -rf $(VIRTUALENV_DIR) -rm -rf dist -rm -rf build + -rm -Rf src/$(PACKAGE_NAME)/__pycache__ + -rm -Rf tests/__pycache__ clean-all: clean @printf "\n\033[36m--- $@: Clean All---\033[0m\n" @@ -52,8 +54,7 @@ clean-all: clean -rm -f main.py -rm -Rf htmlcov -rm .coverage.* - -rm MANIFEST.in - -rm -Rf $(PACKAGE_NAME).egg-info + -rm -Rf src/$(PACKAGE_NAME).egg-info -rm -Rf $(VIRTUALENV_DIR) html: From df1bfe6360d780f899cd132ac7a91a52997d72ee Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 13:22:57 +0000 Subject: [PATCH 13/18] [#67] Updated `.github/workflows/python-package.yml` file: - Removed `Python 3.8` and added `3.13` and `3.14` - Added instruction to install `uv` and use it for installing the requirements. - Updated linting step to use `ruff`. - Updated the `pytest` step to run a more comprehensive command. --- .github/workflows/python-package.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f6134e2..860acb8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v2 @@ -26,14 +26,14 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - - name: Lint with flake8 + pip install uv + if [ -f requirements-dev.txt ]; then uv pip install -r requirements-dev.txt; fi + - name: Lint with ruff run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + ruff check --statistics ./src # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + ruff check --statistics --exit-zero ./src - name: Test with pytest run: | - pytest + PYTHONPATH=./src pytest -n auto -W ignore --color=yes --cov-report term --cov-report html --cov=src/timecode; From 7d817fc7a1dda623b7d22a9d448d750750778bce Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 16:29:30 +0000 Subject: [PATCH 14/18] [#67] Updated github workflow to reflect the change of the `master` branch renamed to `main`. --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 860acb8..7027798 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,9 +5,9 @@ name: Python package on: push: - branches: [ master, develop ] + branches: [ main, develop ] pull_request: - branches: [ master, develop ] + branches: [ main, develop ] jobs: build: From 180e1a0bef627177a289d298e95d5a267e079157 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 16:50:49 +0000 Subject: [PATCH 15/18] [#67] Updated github workflow to install packages to system wide Python. --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 7027798..bfe062f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: run: | python -m pip install --upgrade pip pip install uv - if [ -f requirements-dev.txt ]; then uv pip install -r requirements-dev.txt; fi + if [ -f requirements-dev.txt ]; then uv pip install -r requirements-dev.txt --system; fi - name: Lint with ruff run: | # stop the build if there are Python syntax errors or undefined names From c4decce3a935c00de1c953480c151f03acbe72b5 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 16:53:07 +0000 Subject: [PATCH 16/18] [#67] Updated the module level imports to not to break backwards compatibility. --- src/timecode/__init__.py | 3 ++- tests/test_timecode.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/timecode/__init__.py b/src/timecode/__init__.py index e010457..b478912 100644 --- a/src/timecode/__init__.py +++ b/src/timecode/__init__.py @@ -1 +1,2 @@ -from timecode._version import __version__ \ No newline at end of file +from timecode._version import __version__ +from timecode.timecode import Timecode, TimecodeError \ No newline at end of file diff --git a/tests/test_timecode.py b/tests/test_timecode.py index 46299f6..f6ccbfe 100644 --- a/tests/test_timecode.py +++ b/tests/test_timecode.py @@ -1,7 +1,7 @@ #!-*- coding: utf-8 -*- import pytest -from timecode.timecode import Timecode, TimecodeError +from timecode import Timecode, TimecodeError @pytest.mark.parametrize( From 71406d72d46c7b9a65bde1822bec68f29f5ef6fe Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 19:04:57 +0000 Subject: [PATCH 17/18] [#67] Fixed all linting errors on the `timecode` module. --- src/timecode/__init__.py | 4 +- src/timecode/_version.py | 2 +- src/timecode/timecode.py | 436 +++++++++++++++++++-------------------- 3 files changed, 220 insertions(+), 222 deletions(-) diff --git a/src/timecode/__init__.py b/src/timecode/__init__.py index b478912..49ab1a3 100644 --- a/src/timecode/__init__.py +++ b/src/timecode/__init__.py @@ -1,2 +1,2 @@ -from timecode._version import __version__ -from timecode.timecode import Timecode, TimecodeError \ No newline at end of file +from timecode._version import __version__ # noqa: F401 +from timecode.timecode import Timecode, TimecodeError # noqa: F401 diff --git a/src/timecode/_version.py b/src/timecode/_version.py index 21aad4a..a31d9c8 100644 --- a/src/timecode/_version.py +++ b/src/timecode/_version.py @@ -6,7 +6,7 @@ _VERSION = None _VERSION_FILE = os.path.join(os.path.dirname(__file__), "VERSION") if os.path.isfile(_VERSION_FILE): - with open(_VERSION_FILE, "r") as _version_file: + with open(_VERSION_FILE) as _version_file: _VERSION = _version_file.read().strip() __version__ = _VERSION or "0.0.0" """str: The version of the package.""" diff --git a/src/timecode/timecode.py b/src/timecode/timecode.py index 6e69872..5b5cd04 100644 --- a/src/timecode/timecode.py +++ b/src/timecode/timecode.py @@ -1,68 +1,61 @@ -#!-*- coding: utf-8 -*- -# The MIT License (MIT) -# -# Copyright (c) 2014 Joshua Banton and PyTimeCode developers -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - - -from fractions import Fraction +"""Timecode class for handling timecode calculations.""" + +# Standard Library Imports +from __future__ import annotations + import sys -from typing import Optional, Tuple, Union, overload +from contextlib import suppress +from typing import TYPE_CHECKING, overload -try: +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +with suppress(ImportError): from typing import Literal -except ImportError: - pass -class Timecode(object): +if TYPE_CHECKING: + from collections.abc import Iterator + from fractions import Fraction + + +class Timecode: """The main timecode class. - Does all the calculation over frames, so the main data it holds is frames, then when - required it converts the frames to a timecode by using the frame rate setting. + Does all the calculation over frames, so the main data it holds is frames, + then when required it converts the frames to a timecode by using the frame + rate setting. Args: - framerate (Union[str, int, float, Fraction]): The frame rate of the Timecode - instance. If a str is given it should be one of ['23.976', '23.98', '24', - '25', '29.97', '30', '50', '59.94', '60', 'NUMERATOR/DENOMINATOR', ms'] - where "ms" equals to 1000 fps. Otherwise, any integer or Fractional value is - accepted. Can not be skipped. Setting the framerate will automatically set - the :attr:`.drop_frame` attribute to correct value. - start_timecode (Union[None, str]): The start timecode. Use this to be able to - set the timecode of this Timecode instance. It can be skipped and then the - frames attribute will define the timecode, and if it is also skipped then - the start_second attribute will define the start timecode, and if - start_seconds is also skipped then the default value of '00:00:00:00' will - be used. When using 'ms' frame rate, timecodes like '00:11:01.040' use - '.040' as frame number. When used with other frame rates, '.040' represents - a fraction of a second. So '00:00:00.040' at 25fps is 1 frame. - start_seconds (Union[int, float]): A float or integer value showing the seconds. - frames (int): Timecode objects can be initialized with an integer number showing - the total frames. - force_non_drop_frame (bool): If True, uses Non-Dropframe calculation for 29.97 - or 59.94 only. Has no meaning for any other framerate. It is False by - default. + framerate (str | int | float | Fraction): The frame rate of the + Timecode instance. If a str is given it should be one of ['23.976', + '23.98', '24', '25', '29.97', '30', '50', '59.94', '60', + 'NUMERATOR/DENOMINATOR', ms'] where "ms" equals to 1000 fps. + Otherwise, any integer or Fractional value is accepted. Can not be + skipped. Setting the framerate will automatically set the + :attr:`.drop_frame` attribute to correct value. + start_timecode (None | str): The start timecode. Use this to be able to + set the timecode of this Timecode instance. It can be skipped and + then the frames attribute will define the timecode, and if it is + also skipped then the start_second attribute will define the start + timecode, and if start_seconds is also skipped then the default + value of '00:00:00:00' will be used. When using 'ms' frame rate, + timecodes like '00:11:01.040' use '.040' as frame number. When used + with other frame rates, '.040' represents a fraction of a second. + So '00:00:00.040' at 25fps is 1 frame. + start_seconds (int | float): A float or integer value showing the + seconds. + frames (int): Timecode objects can be initialized with an integer + number showing the total frames. + force_non_drop_frame (bool): If True, uses Non-Dropframe calculation + for 29.97 or 59.94 only. Has no meaning for any other framerate. It + is False by default. """ @staticmethod - def _is_ntsc_rate(fps: float) -> Tuple[bool, int]: + def _is_ntsc_rate(fps: float) -> tuple[bool, int]: """Check if framerate is NTSC (multiple of 24000/1001 or 30000/1001). NTSC rates follow the pattern: nominal_rate * 1000/1001 @@ -72,8 +65,8 @@ def _is_ntsc_rate(fps: float) -> Tuple[bool, int]: fps (float): The framerate to check. Returns: - tuple: (is_ntsc, int_framerate) where is_ntsc is True if this is an NTSC rate, - and int_framerate is the rounded integer framerate. + tuple: (is_ntsc, int_framerate) where is_ntsc is True if this is an + NTSC rate, and int_framerate is the rounded integer framerate. """ # Calculate what the integer framerate would be if this is NTSC int_fps = round(fps * 1001 / 1000) @@ -88,38 +81,36 @@ def _is_ntsc_rate(fps: float) -> Tuple[bool, int]: def __init__( self, - framerate: Union[str, int, float, Fraction], - start_timecode: Optional[str] = None, - start_seconds: Optional[Union[int, float]] = None, - frames: Optional[int] = None, + framerate: str | float | Fraction, + start_timecode: None | str = None, + start_seconds: None | float = None, + frames: None | int = None, force_non_drop_frame: bool = False, - ): - + ) -> None: self.force_non_drop_frame = force_non_drop_frame self.drop_frame = False self.ms_frame = False self.fraction_frame = False - self._int_framerate : Union[None, int] = None - self._framerate : Union[None, str, int, float, Fraction] = None + self._int_framerate: None | int = None + self._framerate: None | str | int | float | Fraction = None self.framerate = framerate # type: ignore - self._frames : Union[None, int] = None + self._frames: None | int = None # attribute override order # start_timecode > frames > start_seconds if start_timecode: self.frames = self.tc_to_frames(start_timecode) + elif frames is not None: + self.frames = frames + elif start_seconds is not None: + if start_seconds == 0: + raise ValueError("``start_seconds`` argument can not be 0") + self.frames = self.float_to_tc(start_seconds) else: - if frames is not None: - self.frames = frames - elif start_seconds is not None: - if start_seconds == 0: - raise ValueError("``start_seconds`` argument can not be 0") - self.frames = self.float_to_tc(start_seconds) - else: - # use default value of 00:00:00:00 - self.frames = self.tc_to_frames("00:00:00:00") + # use default value of 00:00:00:00 + self.frames = self.tc_to_frames("00:00:00:00") @property def frames(self) -> int: @@ -141,15 +132,14 @@ def frames(self, frames: int) -> None: # validate the frames value if not isinstance(frames, int): raise TypeError( - "%s.frames should be a positive integer bigger " - "than zero, not a %s" - % (self.__class__.__name__, frames.__class__.__name__) + f"{self.__class__.__name__}.frames should be a positive integer bigger " + f"than zero, not a {frames.__class__.__name__}" ) if frames <= 0: raise ValueError( - "%s.frames should be a positive integer bigger " - "than zero, not %s" % (self.__class__.__name__, frames) + f"{self.__class__.__name__}.frames should be a positive " + f"integer bigger than zero, not {frames}" ) self._frames = frames @@ -163,12 +153,12 @@ def framerate(self) -> str: return self._framerate # type: ignore @framerate.setter - def framerate(self, framerate: Union[int, float, str, Tuple[int, int], Fraction]) -> None: + def framerate(self, framerate: float | str | tuple[int, int] | Fraction) -> None: """Set the framerate attribute. Args: - framerate (Union[int, float, str, tuple, Fraction]): Several different type - is accepted for this argument: + framerate (int | float | str | tuple[int, int] | Fraction): Several + different type is accepted for this argument: int, float: It is directly used. str: Is used for setting DF Timecodes and possible values are @@ -181,8 +171,8 @@ def framerate(self, framerate: Union[int, float, str, Tuple[int, int], Fraction] then Fraction is also accepted. """ # Convert rational frame rate to float, defaults to None if not Fraction-like - numerator = getattr(framerate, 'numerator', None) - denominator = getattr(framerate, 'denominator', None) + numerator = getattr(framerate, "numerator", None) + denominator = getattr(framerate, "denominator", None) try: if "/" in framerate: # type: ignore @@ -243,12 +233,12 @@ def set_fractional(self, state: bool) -> None: """ self.fraction_frame = state - def set_timecode(self, timecode: Union[str, "Timecode"]) -> None: + def set_timecode(self, timecode: str | Timecode) -> None: """Set the frames by using the given timecode. Args: - timecode (Union[str, Timecode]): Either a str representation of a Timecode - or a Timecode instance. + timecode (str | Timecode): Either a str representation of a + Timecode or a Timecode instance. """ self.frames = self.tc_to_frames(timecode) @@ -264,12 +254,12 @@ def float_to_tc(self, seconds: float) -> int: """ return int(seconds * self._int_framerate) - def tc_to_frames(self, timecode: Union[str, "Timecode"]) -> int: + def tc_to_frames(self, timecode: str | Timecode) -> int: """Convert the given Timecode to frames. Args: - timecode (Union[str, Timecode]): Either a str representing a Timecode or a - Timecode instance. + timecode (str | Timecode): Either a str representing a Timecode or + a Timecode instance. Returns: int: The number of frames in the given Timecode. @@ -287,17 +277,14 @@ def tc_to_frames(self, timecode: Union[str, "Timecode"]) -> int: if self.drop_frame: timecode = ";".join(timecode.rsplit(":", 1)) - if self.framerate != "frames": - ffps = float(self.framerate) - else: - ffps = float(self._int_framerate) + ffps = ( + float(self.framerate) + if self.framerate != "frames" + else float(self._int_framerate) + ) - if self.drop_frame: - # Number of drop frames is 6% of framerate rounded to nearest - # integer - drop_frames = int(round(ffps * 0.066666)) - else: - drop_frames = 0 + # Number of drop frames is 6% of framerate rounded to nearest integer + drop_frames = round(ffps * 0.066666) if self.drop_frame else 0 # We don't need the exact framerate anymore, we just need it rounded to # nearest integer @@ -317,7 +304,7 @@ def tc_to_frames(self, timecode: Union[str, "Timecode"]) -> int: self.fraction_frame = True fraction = timecode.rsplit(".", 1)[1] - frames = int(round(float("." + fraction) * ffps)) + frames = round(float("." + fraction) * ffps) frame_number = ( (hour_frames * hours) @@ -328,11 +315,15 @@ def tc_to_frames(self, timecode: Union[str, "Timecode"]) -> int: return frame_number + 1 # frames - def frames_to_tc(self, frames: int, skip_rollover: bool = False) -> Tuple[int, int, int, Union[float, int]]: + def frames_to_tc( + self, frames: int, skip_rollover: bool = False + ) -> tuple[int, int, int, int | float]: """Convert frames back to timecode. Args: frames (int): Number of frames. + skip_rollover (bool): If True, the frame number will not rollover + after 24 hours. Returns: tuple: A tuple containing the hours, minutes, seconds and frames @@ -341,16 +332,16 @@ def frames_to_tc(self, frames: int, skip_rollover: bool = False) -> Tuple[int, i # Number of frames to drop on the minute marks is the nearest # integer to 6% of the framerate ffps = float(self.framerate) - drop_frames = int(round(ffps * 0.066666)) + drop_frames = round(ffps * 0.066666) else: ffps = float(self._int_framerate) drop_frames = 0 # Number of frames per ten minutes - frames_per_10_minutes = int(round(ffps * 60 * 10)) + frames_per_10_minutes = round(ffps * 60 * 10) # Number of frames in a day - timecode rolls over after 24 hours - frames_per_24_hours = int(round(ffps * 60 * 60 * 24)) + frames_per_24_hours = round(ffps * 60 * 60 * 24) # Number of frames per minute is the round of the framerate * 60 minus # the number of dropped frames @@ -375,17 +366,17 @@ def frames_to_tc(self, frames: int, skip_rollover: bool = False) -> Tuple[int, i ifps = self._int_framerate - frs: Union[int, float] = frame_number % ifps + frs: int | float = frame_number % ifps if self.fraction_frame: frs = round(frs / float(ifps), 3) secs = int((frame_number // ifps) % 60) mins = int(((frame_number // ifps) // 60) % 60) - hrs = int((((frame_number // ifps) // 60) // 60)) + hrs = int(((frame_number // ifps) // 60) // 60) return hrs, mins, secs, frs - def tc_to_string(self, hrs: int, mins: int, secs: int, frs: Union[float, int]) -> str: + def tc_to_string(self, hrs: int, mins: int, secs: int, frs: float) -> str: """Return the string representation of a Timecode with given info. Args: @@ -398,7 +389,7 @@ def tc_to_string(self, hrs: int, mins: int, secs: int, frs: Union[float, int]) - str: The string representation of this Timecode.ßß """ if self.fraction_frame: - return "{hh:02d}:{mm:02d}:{ss:06.3f}".format(hh=hrs, mm=mins, ss=secs + frs) + return f"{hrs:02d}:{mins:02d}:{secs + frs:06.3f}" ff = "{:02d}" if self.ms_frame: @@ -408,19 +399,17 @@ def tc_to_string(self, hrs: int, mins: int, secs: int, frs: Union[float, int]) - hrs, mins, secs, self.frame_delimiter, frs ) - # to maintain python 3.7 compatibility (no literal type yet!) - # only use overload in 3.8+ - if sys.version_info >= (3, 8): - @overload - def to_systemtime(self, as_float: Literal[True]) -> float: - pass + @overload + def to_systemtime(self, as_float: Literal[True]) -> float: + pass - @overload - def to_systemtime(self, as_float: Literal[False]) -> str: - pass + @overload + def to_systemtime(self, as_float: Literal[False]) -> str: + pass - def to_systemtime(self, as_float: bool = False) -> Union[str, float]: # type:ignore + def to_systemtime(self, as_float: bool = False) -> str | float: # type:ignore """Convert a Timecode to the video system timestamp. + For NTSC rates, the video system time is not the wall-clock one. Args: @@ -430,28 +419,28 @@ def to_systemtime(self, as_float: bool = False) -> Union[str, float]: # type:ig str: The "system time" timestamp of the Timecode. """ if self.ms_frame: - return self.float-(1e-3) if as_float else str(self) + return self.float - (1e-3) if as_float else str(self) hh, mm, ss, ff = self.frames_to_tc(self.frames + 1, skip_rollover=True) - framerate = float(self.framerate) if self._ntsc_framerate else self._int_framerate - ms = ff/framerate + framerate = ( + float(self.framerate) if self._ntsc_framerate else self._int_framerate + ) + ms = ff / framerate if as_float: - return (hh*3600 + mm*60 + ss + ms) - return "{:02d}:{:02d}:{:02d}.{:03d}".format(hh, mm, ss, round(ms*1000)) - - # to maintain python 3.7 compatibility (no literal type yet!) - # only use typing.Literal in 3.8+ - if sys.version_info >= (3, 8): - @overload # type: ignore # noqa - def to_realtime(self, as_float: Literal[True]) -> float: - pass + return hh * 3600 + mm * 60 + ss + ms + return f"{hh:02d}:{mm:02d}:{ss:02d}.{round(ms * 1000):03d}" - @overload # type: ignore # noqa - def to_realtime(self, as_float: Literal[False]) -> str: - pass + @overload + def to_realtime(self, as_float: Literal[True]) -> float: + pass + + @overload + def to_realtime(self, as_float: Literal[False]) -> str: + pass - def to_realtime(self, as_float: bool = False) -> Union[str, float]: # type:ignore + def to_realtime(self, as_float: bool = False) -> str | float: # type:ignore """Convert a Timecode to a "real time" timestamp. + Reference: SMPTE 12-1 §5.1.2 Args: @@ -460,11 +449,11 @@ def to_realtime(self, as_float: bool = False) -> Union[str, float]: # type:igno Returns: str: The "real time" timestamp of the Timecode. """ - #float property is in the video system time grid + # float property is in the video system time grid ts_float = self.float if self.ms_frame: - return ts_float-(1e-3) if as_float else str(self) + return ts_float - (1e-3) if as_float else str(self) # "int_framerate" frames is one second in NTSC time if self._ntsc_framerate: @@ -472,16 +461,26 @@ def to_realtime(self, as_float: bool = False) -> Union[str, float]: # type:igno if as_float: return ts_float - f_fmtdivmod = lambda x: (int(x[0]), x[1]) - hh, ts_float = f_fmtdivmod(divmod(ts_float, 3600)) - mm, ts_float = f_fmtdivmod(divmod(ts_float, 60)) - ss, ts_float = f_fmtdivmod(divmod(ts_float, 1)) - ms = round(ts_float*1000) + def f_fmt_divmod(x: tuple[int, float]) -> tuple[int, float]: + """Helper to format divmod results. - return "{:02d}:{:02d}:{:02d}.{:03d}".format(hh, mm, ss, ms) + Args: + x (tuple): The divmod result. + + Returns: + tuple[int, float]: Formatted divmod result. + """ + return (int(x[0]), x[1]) + + hh, ts_float = f_fmt_divmod(divmod(ts_float, 3600)) + mm, ts_float = f_fmt_divmod(divmod(ts_float, 60)) + ss, ts_float = f_fmt_divmod(divmod(ts_float, 1)) + ms = round(ts_float * 1000) + + return f"{hh:02d}:{mm:02d}:{ss:02d}.{ms:03d}" @classmethod - def parse_timecode(cls, timecode: Union[int, str]) -> Tuple[int, int, int, int]: + def parse_timecode(cls, timecode: int | str) -> tuple[int, int, int, int]: """Parse the given timecode string. This uses the frame separator do decide if this is a NDF, DF or a @@ -492,7 +491,7 @@ def parse_timecode(cls, timecode: Union[int, str]) -> Tuple[int, int, int, int]: Timecode. Args: - timecode (Union[int, str]): If an integer is given it is converted to hex + timecode (int | str): If an integer is given it is converted to hex and the hours, minutes, seconds and frames are extracted from the hex representation. If a str is given it should follow one of the SMPTE timecode formats.ß @@ -504,7 +503,7 @@ def parse_timecode(cls, timecode: Union[int, str]) -> Tuple[int, int, int, int]: if isinstance(timecode, int): hex_repr = hex(timecode) # fix short string - hex_repr = "0x%s" % (hex_repr[2:].zfill(8)) + hex_repr = f"0x{hex_repr[2:].zfill(8)}" hrs, mins, secs, frs = tuple( map(int, [hex_repr[i : i + 2] for i in range(2, 10, 2)]) ) @@ -529,13 +528,12 @@ def frame_delimiter(self) -> str: if self.drop_frame: return ";" - elif self.ms_frame or self.fraction_frame: + if self.ms_frame or self.fraction_frame: return "." - else: - return ":" + return ":" - def __iter__(self): + def __iter__(self) -> Iterator[Self]: """Yield and iterator. Yields: @@ -543,7 +541,7 @@ def __iter__(self): """ yield self - def next(self) -> "Timecode": + def next(self) -> Self: """Add one frame to this Timecode to go the next frame. Returns: @@ -553,7 +551,7 @@ def next(self) -> "Timecode": self.add_frames(1) return self - def back(self) -> "Timecode": + def back(self) -> Self: """Subtract one frame from this Timecode to go back one frame. Returns: @@ -598,81 +596,81 @@ def div_frames(self, frames: int) -> None: """ self.frames = int(self.frames / frames) - def __eq__(self, other: Union[int, str, "Timecode", object]) -> bool: + def __eq__(self, other: int | str | Timecode | object) -> bool: """Override the equality operator. Args: - other (Union[int, str, Timecode]): Either and int representing the number of - frames, a str representing the start time of a Timecode with the same - frame rate of this one, or a Timecode to compare with the number of - frames. + other (int | str | Timecode): Either and int representing the + number of frames, a str representing the start time of a + Timecode with the same frame rate of this one, or a Timecode to + compare with the number of frames. Returns: bool: True if the other is equal to this Timecode instance. """ if isinstance(other, Timecode): return self.framerate == other.framerate and self.frames == other.frames - elif isinstance(other, str): + if isinstance(other, str): new_tc = Timecode(self.framerate, other) return self.__eq__(new_tc) - elif isinstance(other, int): + if isinstance(other, int): return self.frames == other - else: - return False + return False - def __ge__(self, other: Union[int, str, "Timecode", object]) -> bool: + def __ge__(self, other: int | str | Timecode | object) -> bool: """Override greater than or equal to operator. Args: - other (Union[int, str, Timecode]): Either and int representing the number of - frames, a str representing the start time of a Timecode with the same - frame rate of this one, or a Timecode to compare with the number of - frames. + other (int | str | Timecode): Either and int representing the + number of frames, a str representing the start time of a + Timecode with the same frame rate of this one, or a Timecode to + compare with the number of frames. Returns: - bool: True if the other is greater than or equal to this Timecode instance. + bool: True if the other is greater than or equal to this Timecode + instance. """ if isinstance(other, Timecode): return self.framerate == other.framerate and self.frames >= other.frames - elif isinstance(other, str): + if isinstance(other, str): new_tc = Timecode(self.framerate, other) return self.frames >= new_tc.frames - elif isinstance(other, int): + if isinstance(other, int): return self.frames >= other - else: - raise TypeError( - "'>=' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) - ) + raise TypeError( + "'>=' not supported between instances of 'Timecode' and " + f"'{other.__class__.__name__}'" + ) - def __gt__(self, other: Union[int, str, "Timecode"]) -> bool: + def __gt__(self, other: int | str | Timecode) -> bool: """Override greater than operator. Args: - other (Union[int, str, Timecode]): Either and int representing the number of - frames, a str representing the start time of a Timecode with the same - frame rate of this one, or a Timecode to compare with the number of - frames. + other (int | str, Timecode): Either and int representing the number + of frames, a str representing the start time of a Timecode with + the same frame rate of this one, or a Timecode to compare with + the number of frames. Returns: bool: True if the other is greater than this Timecode instance. """ if isinstance(other, Timecode): return self.framerate == other.framerate and self.frames > other.frames - elif isinstance(other, str): + if isinstance(other, str): new_tc = Timecode(self.framerate, other) return self.frames > new_tc.frames - elif isinstance(other, int): + if isinstance(other, int): return self.frames > other - else: - raise TypeError( - "'>' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) - ) + raise TypeError( + "'>' not supported between instances of 'Timecode' and " + f"'{other.__class__.__name__}'" + ) - def __le__(self, other: Union[int, str, "Timecode", object]) -> bool: + def __le__(self, other: int | str | Timecode | object) -> bool: """Override less or equal to operator. Args: - other (Union[int, str, Timecode]): Either and int representing the number of + other (int | str | Timecode): Either and int representing the number of frames, a str representing the start time of a Timecode with the same frame rate of this one, or a Timecode to compare with the number of frames. @@ -682,21 +680,21 @@ def __le__(self, other: Union[int, str, "Timecode", object]) -> bool: """ if isinstance(other, Timecode): return self.framerate == other.framerate and self.frames <= other.frames - elif isinstance(other, str): + if isinstance(other, str): new_tc = Timecode(self.framerate, other) return self.frames <= new_tc.frames - elif isinstance(other, int): + if isinstance(other, int): return self.frames <= other - else: - raise TypeError( - "'<' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) - ) + raise TypeError( + "'<' not supported between instances of 'Timecode' and " + f"'{other.__class__.__name__}'" + ) - def __lt__(self, other: Union[int, str, "Timecode"]) -> bool: + def __lt__(self, other: int | str | Timecode) -> bool: """Override less than operator. Args: - other (Union[int, str, Timecode]): Either and int representing the number of + other (int | str | Timecode): Either and int representing the number of frames, a str representing the start time of a Timecode with the same frame rate of this one, or a Timecode to compare with the number of frames. @@ -706,21 +704,21 @@ def __lt__(self, other: Union[int, str, "Timecode"]) -> bool: """ if isinstance(other, Timecode): return self.framerate == other.framerate and self.frames < other.frames - elif isinstance(other, str): + if isinstance(other, str): new_tc = Timecode(self.framerate, other) return self.frames < new_tc.frames - elif isinstance(other, int): + if isinstance(other, int): return self.frames < other - else: - raise TypeError( - "'<=' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) - ) + raise TypeError( + "'<=' not supported between instances of 'Timecode' and " + f"'{other.__class__.__name__}'" + ) - def __add__(self, other: Union["Timecode", int]) -> "Timecode": + def __add__(self, other: int | Timecode) -> Timecode: """Return a new Timecode with the given timecode or frames added to this one. Args: - other (Union[int, Timecode]): Either and int value or a Timecode in which + other (int | Timecode): Either and int value or a Timecode in which the frames are used for the calculation. Raises: @@ -739,16 +737,16 @@ def __add__(self, other: Union["Timecode", int]) -> "Timecode": tc.add_frames(other) else: raise TimecodeError( - "Type {} not supported for arithmetic.".format(other.__class__.__name__) + f"Type {other.__class__.__name__} not supported for arithmetic." ) return tc - def __sub__(self, other: Union["Timecode", int]) -> "Timecode": + def __sub__(self, other: int | Timecode) -> Timecode: """Return a new Timecode instance with subtracted value. Args: - other (Union[int, Timecode]): The number to subtract, either an integer or + other (int | Timecode): The number to subtract, either an integer or another Timecode in which the number of frames is subtracted. Raises: @@ -763,17 +761,17 @@ def __sub__(self, other: Union["Timecode", int]) -> "Timecode": subtracted_frames = self.frames - other else: raise TimecodeError( - "Type {} not supported for arithmetic.".format(other.__class__.__name__) + f"Type {other.__class__.__name__} not supported for arithmetic." ) tc = Timecode(self.framerate, frames=abs(subtracted_frames)) tc.drop_frame = self.drop_frame return tc - def __mul__(self, other: Union["Timecode", int]) -> "Timecode": + def __mul__(self, other: int | Timecode) -> Timecode: """Return a new Timecode instance with multiplied value. Args: - other (Union[int, Timecode]): The multiplier either an integer or another + other (int | Timecode): The multiplier either an integer or another Timecode in which the number of frames is used as the multiplier. Raises: @@ -788,17 +786,17 @@ def __mul__(self, other: Union["Timecode", int]) -> "Timecode": multiplied_frames = self.frames * other else: raise TimecodeError( - "Type {} not supported for arithmetic.".format(other.__class__.__name__) + f"Type {other.__class__.__name__} not supported for arithmetic." ) tc = Timecode(self.framerate, frames=multiplied_frames) tc.drop_frame = self.drop_frame return tc - def __div__(self, other: Union["Timecode", int]) -> "Timecode": + def __div__(self, other: int | Timecode) -> Timecode: """Return a new Timecode instance with divided value. Args: - other (Union[int, Timecode]): The denominator either an integer or another + other (int | Timecode): The denominator either an integer or another Timecode in which the number of frames is used as the denominator. Raises: @@ -813,16 +811,16 @@ def __div__(self, other: Union["Timecode", int]) -> "Timecode": div_frames = int(float(self.frames) / float(other)) else: raise TimecodeError( - "Type {} not supported for arithmetic.".format(other.__class__.__name__) + f"Type {other.__class__.__name__} not supported for arithmetic." ) return Timecode(self.framerate, frames=div_frames) - def __truediv__(self, other: Union["Timecode", int]) -> "Timecode": + def __truediv__(self, other: int | Timecode) -> Timecode: """Return a new Timecode instance with divided value. Args: - other (Union[int, Timecode]): The denominator either an integer or another + other (int | Timecode): The denominator either an integer or another Timecode in which the number of frames is used as the denominator. Returns: @@ -830,7 +828,7 @@ def __truediv__(self, other: Union["Timecode", int]) -> "Timecode": """ return self.__div__(other) - def __repr__(self): + def __repr__(self) -> str: """Return the string representation of this Timecode instance. Returns: @@ -845,7 +843,7 @@ def hrs(self) -> int: Returns: int: The hours part of the timecode. """ - hrs, mins, secs, frs = self.frames_to_tc(self.frames) + hrs, _, _, _ = self.frames_to_tc(self.frames) return hrs @property @@ -855,7 +853,7 @@ def mins(self) -> int: Returns: int: The minutes part of the timecode. """ - hrs, mins, secs, frs = self.frames_to_tc(self.frames) + _, mins, _, _ = self.frames_to_tc(self.frames) return mins @property @@ -865,17 +863,17 @@ def secs(self) -> int: Returns: int: The seconds part of the timecode. """ - hrs, mins, secs, frs = self.frames_to_tc(self.frames) + _, _, secs, _ = self.frames_to_tc(self.frames) return secs @property - def frs(self) -> Union[float, int]: + def frs(self) -> int | float: """Return the frames part of the timecode. Returns: int: The frames part of the timecode. """ - hrs, mins, secs, frs = self.frames_to_tc(self.frames) + _, _, _, frs = self.frames_to_tc(self.frames) return frs @property From d38cbbbf5181edecba8363587528e4d14d7d9c55 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 29 Dec 2025 19:13:50 +0000 Subject: [PATCH 18/18] [#67] Updated `README.md` with fancy badges. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d85f8a1..eafd644 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +![license](https://img.shields.io/pypi/l/timecode.svg) +![pyversion](https://img.shields.io/pypi/pyversions/timecode.svg) +![pypiversion](https://img.shields.io/pypi/v/timecode.svg) +![wheel](https://img.shields.io/pypi/wheel/timecode.svg) + About -----