14
0

Accepting request 1155039 from devel:languages:python

- version update to 2.1.0
  * Fix #110 (broken test) in #113
  * CI: update cibuildwheel; ensure Python-3.12 builds in #116
  * Modernize tooling in #117
- version update to 2.0.0
  * improve build_taglib.py helper script (now supports all platforms)
  * add taglib_version() to the taglib module
  * allow using File as a context manager, optionally saving on exit
  * new property File.is_closed
  * fix #94: Accept os.PathLike in constructor
  * File.path is now a Path object
- Add upgrade_taglib_version.patch
- Skip building for python 3.6 on Leap since it now uses a pyproject.toml

OBS-URL: https://build.opensuse.org/request/show/1155039
OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/python-pytaglib?expand=0&rev=9
This commit is contained in:
2024-03-05 17:51:49 +00:00
committed by Git OBS Bridge
5 changed files with 447 additions and 13 deletions

View File

@@ -1,3 +1,20 @@
-------------------------------------------------------------------
Fri Mar 1 14:49:51 UTC 2024 - Jaime Marquínez Ferrándiz <jaime.marquinez.ferrandiz@fastmail.net>
- version update to 2.1.0
* Fix #110 (broken test) in #113
* CI: update cibuildwheel; ensure Python-3.12 builds in #116
* Modernize tooling in #117
- version update to 2.0.0
* improve build_taglib.py helper script (now supports all platforms)
* add taglib_version() to the taglib module
* allow using File as a context manager, optionally saving on exit
* new property File.is_closed
* fix #94: Accept os.PathLike in constructor
* File.path is now a Path object
- Add upgrade_taglib_version.patch
- Skip building for python 3.6 on Leap since it now uses a pyproject.toml
-------------------------------------------------------------------
Thu Oct 6 22:10:20 UTC 2022 - Yogalakshmi Arunachalam <yarunachalam@suse.com>

View File

@@ -1,7 +1,7 @@
#
# spec file for package python-pytaglib
#
# Copyright (c) 2022 SUSE LLC
# Copyright (c) 2024 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -17,24 +17,29 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%{?sle15_python_module_pythons}
Name: python-pytaglib
Version: 1.5.0
Version: 2.1.0
Release: 0
Summary: Metadata "tagging" library based on TagLib
License: GPL-3.0-only OR MIT
URL: https://github.com/supermihi/pytaglib
Source: https://github.com/supermihi/pytaglib/archive/v%{version}.tar.gz
# PATCH-FIX-UPSTREAM https://github.com/supermihi/pytaglib/pull/123
Patch1: upgrade_taglib_version.patch
BuildRequires: %{python_module Cython}
BuildRequires: %{python_module devel}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module pytest}
BuildRequires: %{python_module setuptools}
BuildRequires: %{python_module setuptools >= 61.0.0}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: gcc-c++
BuildRequires: libtag-devel
BuildRequires: python-rpm-macros
Requires: python-setuptools
Requires(post): update-alternatives
Requires(postun):update-alternatives
Requires(postun): update-alternatives
%python_subpackages
%description
@@ -42,18 +47,20 @@ pytaglib is an audio metadata (“tag”) library for Python.
It relies on the TagLib C++ library.
%prep
%setup -q -n "pytaglib-%{version}"
%autosetup -N -n "pytaglib-%{version}"
%if %{pkg_vcmp libtag-devel >= 2}
%autopatch -p1 1
%endif
# Remove pre-generated source
rm -vf src/taglib.cpp
sed -i -e "1d" src/pyprinttags.py
%build
sed -i "s:\(script_name =\).*:\1 'pyprinttags':" setup.py
export PYTAGLIB_CYTHONIZE=1
%python_build
%pyproject_wheel
%install
%python_install
%pyproject_install
%python_expand %fdupes %{buildroot}%{$python_sitearch}
%python_clone -a %{buildroot}%{_bindir}/pyprinttags
@@ -68,11 +75,11 @@ export LANG=en_US.UTF-8
%python_uninstall_alternative pyprinttags
%files %{python_files}
%license COPYING
%license LICENSE.txt
%doc README.md
%{python_sitearch}/taglib*.so
%{python_sitearch}/pyprinttags.*
%{python_sitearch}/pytaglib-%{version}-py%{python_version}.egg-info/
%{python_sitearch}/pytaglib-%{version}.dist-info/
%pycache_only %{python_sitearch}/__pycache__/pyprinttags.*
%python_alternative %{_bindir}/pyprinttags

View File

@@ -0,0 +1,410 @@
Reference: https://github.com/supermihi/pytaglib/pull/123
Index: pytaglib-2.1.0/.github/workflows/default.yml
===================================================================
--- pytaglib-2.1.0.orig/.github/workflows/default.yml
+++ pytaglib-2.1.0/.github/workflows/default.yml
@@ -6,12 +6,14 @@ jobs:
build:
strategy:
matrix:
- os: [ubuntu-latest, macos-latest, windows-latest]
+ os: [ubuntu-latest, macos-latest] #, windows-latest]
runs-on: ${{ matrix.os }}
env:
- CIBW_SKIP: "*p36-* *p37-*"
+ #CIBW_SKIP: "*p36-* *p37-*"
+ CIBW_BUILD: "cp311-*"
CIBW_ARCHS: auto64
CIBW_ARCHS_MACOS: "x86_64 arm64"
+ MACOSX_DEPLOYMENT_TARGET: "10.14"
steps:
- uses: actions/checkout@v3
- name: Set up Python
@@ -24,9 +26,6 @@ jobs:
with:
path: build/taglib
key: taglib-windows-${{ hashFiles('build_taglib.py') }}
- - name: Install TagLib (Linux)
- if: ${{ runner.os == 'Linux' }}
- run: sudo apt-get install -y libtag1-dev
- name: install pip dependencies (Linux)
if: ${{ runner.os == 'Linux' }}
run: |
Index: pytaglib-2.1.0/build_taglib.py
===================================================================
--- pytaglib-2.1.0.orig/build_taglib.py
+++ pytaglib-2.1.0/build_taglib.py
@@ -1,4 +1,5 @@
import hashlib
+import os
import platform
import shutil
import subprocess
@@ -8,22 +9,24 @@ import urllib.request
from argparse import ArgumentParser
from pathlib import Path
-is_x64 = sys.maxsize > 2**32
+is_x64 = sys.maxsize > 2 ** 32
arch = "x64" if is_x64 else "x32"
system = platform.system()
python_version = platform.python_version()
here = Path(__file__).resolve().parent
-default_taglib_path = here / "build" / "taglib" / f"{system}-{arch}-py{python_version}"
-taglib_version = "1.13.1"
+taglib_version = "2.0"
taglib_release = f"https://github.com/taglib/taglib/archive/refs/tags/v{taglib_version}.tar.gz"
-taglib_sha256sum = "c8da2b10f1bfec2cd7dbfcd33f4a2338db0765d851a50583d410bacf055cfd0b"
+taglib_sha256sum = "e36ea877a6370810b97d84cf8f72b1e4ed205149ab3ac8232d44c850f38a2859"
+
+utfcpp_version = "4.0.5"
+utfcpp_release = f"https://github.com/nemtrif/utfcpp/archive/refs/tags/v{utfcpp_version}.tar.gz"
class Configuration:
def __init__(self):
- self.tl_install_dir = default_taglib_path
self.build_path = here / "build"
+ self.tl_install_dir = self.build_path / "taglib" / f"{system}-{arch}-py{python_version}"
self.clean = False
@property
@@ -31,31 +34,63 @@ class Configuration:
return self.build_path / f"taglib-{taglib_version}.tar.gz"
@property
+ def utfcpp_download_dest(self):
+ return self.build_path / f"utfcpp-{utfcpp_version}.tar.gz"
+
+ @property
def tl_extract_dir(self):
return self.build_path / f"taglib-{taglib_version}"
+ @property
+ def utfcpp_extract_dir(self):
+ return self.build_path / f"utfcpp-{utfcpp_version}"
-def download(config: Configuration):
- target = config.tl_download_dest
+ @property
+ def utfcpp_include_dir(self):
+ return self.utfcpp_extract_dir / "source"
+
+
+def _download_file(url: str, target: Path, sha256sum: str = None):
if target.exists():
print("skipping download, file exists")
- else:
- print(f"downloading taglib {taglib_version} ...")
- response = urllib.request.urlopen(taglib_release)
- data = response.read()
- target.parent.mkdir(exist_ok=True, parents=True)
- target.write_bytes(data)
+ return
+ print(f"downloading {url} ...")
+ response = urllib.request.urlopen(url)
+ data = response.read()
+ target.parent.mkdir(exist_ok=True, parents=True)
+ target.write_bytes(data)
+ if sha256sum is None:
+ return
the_hash = hashlib.sha256(target.read_bytes()).hexdigest()
- assert the_hash == taglib_sha256sum
+ if the_hash != taglib_sha256sum:
+ error = f'checksum of downloaded file ({the_hash}) does not match expected hash ({taglib_sha256sum})'
+ raise RuntimeError(error)
+
+
+def download(config: Configuration):
+ _download_file(taglib_release, config.tl_download_dest, taglib_sha256sum)
+ _download_file(utfcpp_release, config.utfcpp_download_dest)
+
+
+def _extract_tar(archive: Path, target: Path):
+ if target.exists():
+ print(f"extracted directory {target} found; skipping tar")
+ return
+ print(f"extracting {archive} ...")
+ tar = tarfile.open(archive)
+ tar.extractall(target.parent)
def extract(config: Configuration):
- if config.tl_extract_dir.exists():
- print("extracted taglib found. Skipping tar")
- else:
- print("extracting tarball")
- tar = tarfile.open(config.tl_download_dest)
- tar.extractall(config.tl_extract_dir.parent)
+ _extract_tar(config.tl_download_dest, config.tl_extract_dir)
+ _extract_tar(config.utfcpp_download_dest, config.utfcpp_extract_dir)
+
+
+def copy_utfcpp(config: Configuration):
+ target = config.tl_extract_dir / "3rdparty" / "utfcpp"
+ if target.exists():
+ shutil.rmtree(target)
+ shutil.copytree(config.utfcpp_extract_dir, target)
def cmake_clean(config: Configuration):
@@ -85,6 +120,7 @@ def cmake_config(config: Configuration):
elif system == "Linux":
args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON")
args.append(f"-DCMAKE_INSTALL_PREFIX={config.tl_install_dir}")
+ args.append(f"-DCMAKE_CXX_FLAGS=-I{config.tl_extract_dir / '3rdparty' / 'utfcpp' / 'source'}")
args.append(".")
config.tl_install_dir.mkdir(exist_ok=True, parents=True)
call_cmake(config, *args)
@@ -132,15 +168,16 @@ def run():
print(f"building taglib on {system}, arch {arch}, for python {python_version} ...")
config = parse_args()
tag_lib = (
- config.tl_install_dir
- / "lib"
- / ("tag.lib" if system == "Windows" else "libtag.a")
+ config.tl_install_dir
+ / "lib"
+ / ("tag.lib" if system == "Windows" else "libtag.a")
)
if tag_lib.exists() and not config.clean:
print("installed TagLib found, exiting")
return
download(config)
extract(config)
+ copy_utfcpp(config)
cmake_clean(config)
cmake_config(config)
cmake_build(config)
Index: pytaglib-2.1.0/pyproject.toml
===================================================================
--- pytaglib-2.1.0.orig/pyproject.toml
+++ pytaglib-2.1.0/pyproject.toml
@@ -40,4 +40,5 @@ package-dir = { "" = "src" }
[tool.cibuildwheel]
test-extras = ["tests"]
test-command = "pytest {project}/tests"
-before-build = "python build_taglib.py --clean"
\ No newline at end of file
+before-build = "python build_taglib.py --clean"
+skip = "cp36-* cp37-*"
\ No newline at end of file
Index: pytaglib-2.1.0/src/ctypes.pxd
===================================================================
--- pytaglib-2.1.0.orig/src/ctypes.pxd
+++ pytaglib-2.1.0/src/ctypes.pxd
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright 2011-2018 Michael Helmling, michaelhelmling@posteo.de
+# Copyright 2011-2024 Michael Helmling, michaelhelmling@posteo.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
@@ -7,12 +7,9 @@
"""This file contains the external C/C++ definitions used by taglib.pyx."""
-from libc.stddef cimport wchar_t
from libcpp.list cimport list
from libcpp.map cimport map
from libcpp.string cimport string
-from cpython.mem cimport PyMem_Free
-from cpython.object cimport PyObject
cdef extern from 'taglib/tstring.h' namespace 'TagLib::String':
@@ -42,14 +39,19 @@ cdef extern from 'taglib/tpropertymap.h'
StringList& unsupportedData()
int size()
-
+
cdef extern from 'taglib/audioproperties.h' namespace 'TagLib':
cdef cppclass AudioProperties:
- int length()
+ int lengthInMilliseconds()
int bitrate()
int sampleRate()
int channels()
+cdef extern from 'taglib/audioproperties.h' namespace 'TagLib::AudioProperties':
+ cdef enum ReadStyle:
+ Fast = 0
+ Average = 1
+ Accurate = 2
cdef extern from 'taglib/tfile.h' namespace 'TagLib':
cdef cppclass File:
@@ -62,21 +64,30 @@ cdef extern from 'taglib/tfile.h' namesp
void removeUnsupportedProperties(StringList&)
-IF UNAME_SYSNAME == "Windows":
- cdef extern from 'taglib/fileref.h' namespace 'TagLib::FileRef':
- cdef File * create(const wchar_t *) except +
- cdef extern from "Python.h":
- cdef wchar_t *PyUnicode_AsWideCharString(PyObject *path, Py_ssize_t *size)
- cdef inline File* create_wrapper(unicode path):
- cdef wchar_t *wchar_path = PyUnicode_AsWideCharString(<PyObject*>path, NULL)
- cdef File * file = create(wchar_path)
- PyMem_Free(wchar_path)
- return file
-ELSE:
- cdef extern from 'taglib/fileref.h' namespace 'TagLib::FileRef':
- cdef File* create(const char*) except +
- cdef inline File* create_wrapper(unicode path):
- return create(path.encode('utf-8'))
+cdef extern from 'taglib/tiostream.h' namespace 'TagLib':
+ IF UNAME_SYSNAME != "Windows":
+ ctypedef char* FileName
+ ELSE:
+ cdef cppclass FileName:
+ FileName(const char*)
+
+cdef extern from 'taglib/fileref.h' namespace 'TagLib':
+ cdef cppclass FileRef:
+ FileRef(FileName, boolean, ReadStyle) except +
+ File* file()
+
+ AudioProperties *audioProperties()
+ bint save() except +
+ PropertyMap properties()
+ PropertyMap setProperties(PropertyMap&)
+ void removeUnsupportedProperties(StringList&)
+
+cdef inline FileRef* create_wrapper(char* path) except +:
+ IF UNAME_SYSNAME != "Windows":
+ return new FileRef(path, True, ReadStyle.Average)
+ ELSE:
+ cdef FileName fn = FileName(path)
+ return new FileRef(fn, True, ReadStyle.Average)
cdef extern from 'taglib/taglib.h':
int TAGLIB_MAJOR_VERSION
Index: pytaglib-2.1.0/src/taglib.pyx
===================================================================
--- pytaglib-2.1.0.orig/src/taglib.pyx
+++ pytaglib-2.1.0/src/taglib.pyx
@@ -43,12 +43,12 @@ cdef dict propertyMapToDict(ctypes.Prope
cdef class File:
"""Class representing an audio file with metadata ("tags").
-
+
To read tags from an audio file, create a *File* object, passing the file's path to the
constructor (should be a unicode string):
-
+
>>> f = taglib.File('/path/to/file.ogg')
-
+
The tags are stored in the attribute *tags* as a *dict* mapping strings (tag names)
to lists of strings (tag values).
@@ -59,30 +59,30 @@ cdef class File:
as strings (e.g. cover art, proprietary data written by some programs, ...), according
identifiers will be placed into the *unsupported* attribute of the File object. Using the
method *removeUnsupportedProperties*, some or all of those can be removed.
-
+
Additionally, the readonly attributes *length*, *bitrate*, *sampleRate*, and *channels* are
available with their obvious meanings.
>>> print('File length: {}'.format(f.length))
-
+
Changes to the *tags* attribute are stored using the *save* method.
>>> f.save()
"""
- cdef ctypes.File *cFile
+ cdef ctypes.FileRef *cFile
cdef public dict tags
cdef readonly object path
cdef readonly list unsupported
cdef readonly object save_on_exit
def __cinit__(self, path, save_on_exit: bool = False):
- if not isinstance(path, os.PathLike):
- if not isinstance(path, unicode):
- path = path.decode('utf8')
+ if not isinstance(path, Path):
+ if isinstance(path, bytes):
+ path = path.decode('utf-8')
path = Path(path)
self.path = path
- self.cFile = ctypes.create_wrapper(str(self.path))
- if not self.cFile or not self.cFile.isValid():
+ self.cFile = ctypes.create_wrapper(str(path).encode('utf-8'))
+ if self.cFile is NULL or self.cFile.file() is NULL or not self.cFile.file().isValid():
raise OSError(f'Could not read file {path}')
def __init__(self, path, save_on_exit: bool = False):
@@ -97,7 +97,7 @@ cdef class File:
This method is not accessible from Python, and is called only once, immediately after
object creation.
"""
-
+
cdef:
ctypes.PropertyMap cTags = self.cFile.properties()
ctypes.String cString
@@ -109,7 +109,7 @@ cdef class File:
def save(self):
"""Store the tags currently hold in the `tags` attribute into the file.
-
+
If some tags cannot be stored because the underlying metadata format does not support them,
the unsuccesful tags are returned as a "sub-dictionary" of `self.tags` which will be empty
if everything is ok.
@@ -143,7 +143,7 @@ cdef class File:
if not success:
raise OSError('Unable to save tags: Unknown OS error')
return propertyMapToDict(cRemaining)
-
+
def removeUnsupportedProperties(self, properties):
"""This is a direct binding for the corresponding TagLib method."""
if not self.cFile:
@@ -173,32 +173,32 @@ cdef class File:
property length:
def __get__(self):
self.check_closed()
- return self.cFile.audioProperties().length()
-
+ return self.cFile.audioProperties().lengthInMilliseconds() / 1_000
+
property bitrate:
def __get__(self):
self.check_closed()
return self.cFile.audioProperties().bitrate()
-
+
property sampleRate:
def __get__(self):
self.check_closed()
return self.cFile.audioProperties().sampleRate()
-
+
property channels:
def __get__(self):
self.check_closed()
return self.cFile.audioProperties().channels()
-
+
property readOnly:
def __get__(self):
self.check_closed()
- return self.cFile.readOnly()
+ return self.cFile.file().readOnly()
cdef check_closed(self):
if self.is_closed:
raise ValueError('I/O operation on closed file.')
-
+
def __enter__(self):
return self

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3dc3dba61beb7accf6eef3a515c122a817ed2668b3ae03d85598c8d9861bb933
size 469483

3
v2.1.0.tar.gz Normal file
View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bbf42eaa0677d9f1691bc1045d7e5c9d97f6d48defcb0e8f1b9451a0db5a75c8
size 428497