Skip to content

Commit fc697b1

Browse files
committed
Support writing files in Cython's pure python mode
1 parent b6a3e5e commit fc697b1

File tree

10 files changed

+117
-112
lines changed

10 files changed

+117
-112
lines changed

.github/workflows/smoke.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,9 @@ jobs:
121121
. $CONDA/etc/profile.d/conda.sh
122122
conda config --set always_yes true
123123
conda config --add channels conda-forge
124+
conda config --add channels scientific-python-nightly-wheels
124125
conda create -q -n pyav \
125-
cython \
126+
cython==3.1.0b0 \
126127
numpy \
127128
pillow \
128129
pytest \

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
python-version: "3.13"
1414
- name: Build source package
1515
run: |
16-
pip install setuptools cython
16+
pip install -U --pre cython setuptools
1717
python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor
1818
PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist
1919
- name: Upload source package

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ default: build
1313

1414

1515
build:
16-
$(PIP) install -U cython setuptools
16+
$(PIP) install -U --pre cython setuptools
1717
CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug
1818

1919
clean:

av/filter/loudnorm.pxd

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
from av.audio.stream cimport AudioStream
22

33

4+
cdef extern from "libavcodec/avcodec.h":
5+
ctypedef struct AVCodecContext:
6+
pass
7+
8+
cdef extern from "libavformat/avformat.h":
9+
ctypedef struct AVFormatContext:
10+
pass
11+
12+
cdef extern from "loudnorm_impl.h":
13+
char* loudnorm_get_stats(
14+
AVFormatContext* fmt_ctx,
15+
int audio_stream_index,
16+
const char* loudnorm_args
17+
) nogil
18+
419
cpdef bytes stats(str loudnorm_args, AudioStream stream)

av/filter/loudnorm.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import cython
2+
from cython.cimports.av.audio.stream import AudioStream
3+
from cython.cimports.av.container.core import Container
4+
from cython.cimports.libc.stdlib import free
5+
6+
from av.logging import get_level, set_level
7+
8+
9+
@cython.ccall
10+
def stats(loudnorm_args: str, stream: AudioStream) -> bytes:
11+
"""
12+
Get loudnorm statistics for an audio stream.
13+
14+
Args:
15+
loudnorm_args (str): Arguments for the loudnorm filter (e.g. "i=-24.0:lra=7.0:tp=-2.0")
16+
stream (AudioStream): Input audio stream to analyze
17+
18+
Returns:
19+
bytes: JSON string containing the loudnorm statistics
20+
"""
21+
22+
if "print_format=json" not in loudnorm_args:
23+
loudnorm_args = loudnorm_args + ":print_format=json"
24+
25+
container: Container = stream.container
26+
format_ptr: cython.pointer[AVFormatContext] = container.ptr
27+
container.ptr = cython.NULL # Prevent double-free
28+
29+
stream_index: cython.int = stream.index
30+
py_args: bytes = loudnorm_args.encode("utf-8")
31+
c_args: cython.p_const_char = py_args
32+
result: cython.p_char
33+
34+
# Save log level since C function overwrite it.
35+
level = get_level()
36+
37+
with cython.nogil:
38+
result = loudnorm_get_stats(format_ptr, stream_index, c_args)
39+
40+
if result == cython.NULL:
41+
raise RuntimeError("Failed to get loudnorm stats")
42+
43+
py_result = result[:] # Make a copy of the string
44+
free(result) # Free the C string
45+
46+
set_level(level)
47+
48+
return py_result

av/filter/loudnorm.pyx

Lines changed: 0 additions & 69 deletions
This file was deleted.

av/packet.pyx renamed to av/packet.py

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
1-
cimport libav as lib
1+
import cython
2+
from cython.cimports import libav as lib
3+
from cython.cimports.av.bytesource import bytesource
4+
from cython.cimports.av.error import err_check
5+
from cython.cimports.av.opaque import opaque_container
6+
from cython.cimports.av.utils import avrational_to_fraction, to_avrational
27

3-
from av.bytesource cimport bytesource
4-
from av.error cimport err_check
5-
from av.opaque cimport opaque_container
6-
from av.utils cimport avrational_to_fraction, to_avrational
7-
8-
9-
cdef class Packet(Buffer):
108

9+
@cython.cclass
10+
class Packet(Buffer):
1111
"""A packet of encoded data within a :class:`~av.format.Stream`.
1212
1313
This may, or may not include a complete object within a stream.
1414
:meth:`decode` must be called to extract encoded data.
15-
1615
"""
1716

1817
def __cinit__(self, input=None):
19-
with nogil:
18+
with cython.nogil:
2019
self.ptr = lib.av_packet_alloc()
2120

21+
def __dealloc__(self):
22+
with cython.nogil:
23+
lib.av_packet_free(cython.address(self.ptr))
24+
2225
def __init__(self, input=None):
23-
cdef size_t size = 0
24-
cdef ByteSource source = None
26+
size: cython.size_t = 0
27+
source: ByteSource = None
2528

2629
if input is None:
2730
return
@@ -41,24 +44,24 @@ def __init__(self, input=None):
4144
# instead of its data.
4245
# self.source = source
4346

44-
def __dealloc__(self):
45-
with nogil:
46-
lib.av_packet_free(&self.ptr)
47-
4847
def __repr__(self):
4948
stream = self._stream.index if self._stream else 0
5049
return (
51-
f"<av.{self.__class__.__name__} of #{stream}, dts={self.dts},"
50+
f"av.{self.__class__.__name__} of #{stream}, dts={self.dts},"
5251
f" pts={self.pts}; {self.ptr.size} bytes at 0x{id(self):x}>"
5352
)
5453

5554
# Buffer protocol.
56-
cdef size_t _buffer_size(self):
55+
@cython.cfunc
56+
def _buffer_size(self) -> cython.size_t:
5757
return self.ptr.size
58-
cdef void* _buffer_ptr(self):
58+
59+
@cython.cfunc
60+
def _buffer_ptr(self) -> cython.p_void:
5961
return self.ptr.data
6062

61-
cdef _rebase_time(self, lib.AVRational dst):
63+
@cython.cfunc
64+
def _rebase_time(self, dst: lib.AVRational):
6265
if not dst.num:
6366
raise ValueError("Cannot rebase to zero time.")
6467

@@ -92,7 +95,7 @@ def stream(self):
9295
return self._stream
9396

9497
@stream.setter
95-
def stream(self, Stream stream):
98+
def stream(self, stream: Stream):
9699
self._stream = stream
97100
self.ptr.stream_index = stream.ptr.index
98101

@@ -103,11 +106,11 @@ def time_base(self):
103106
104107
:type: fractions.Fraction
105108
"""
106-
return avrational_to_fraction(&self._time_base)
109+
return avrational_to_fraction(cython.address(self._time_base))
107110

108111
@time_base.setter
109112
def time_base(self, value):
110-
to_avrational(value, &self._time_base)
113+
to_avrational(value, cython.address(self._time_base))
111114

112115
@property
113116
def pts(self):
@@ -116,7 +119,7 @@ def pts(self):
116119
117120
This is the time at which the packet should be shown to the user.
118121
119-
:type: int
122+
:type: int | None
120123
"""
121124
if self.ptr.pts != lib.AV_NOPTS_VALUE:
122125
return self.ptr.pts
@@ -133,7 +136,7 @@ def dts(self):
133136
"""
134137
The decoding timestamp in :attr:`time_base` units for this packet.
135138
136-
:type: int
139+
:type: int | None
137140
"""
138141
if self.ptr.dts != lib.AV_NOPTS_VALUE:
139142
return self.ptr.dts
@@ -152,7 +155,7 @@ def pos(self):
152155
153156
Returns `None` if it is not known.
154157
155-
:type: int
158+
:type: int | None
156159
"""
157160
if self.ptr.pos != -1:
158161
return self.ptr.pos
@@ -221,14 +224,15 @@ def is_disposable(self):
221224

222225
@property
223226
def opaque(self):
224-
if self.ptr.opaque_ref is not NULL:
225-
return opaque_container.get(<char *> self.ptr.opaque_ref.data)
227+
if self.ptr.opaque_ref is not cython.NULL:
228+
return opaque_container.get(
229+
cython.cast(cython.p_char, self.ptr.opaque_ref.data)
230+
)
226231

227232
@opaque.setter
228233
def opaque(self, v):
229-
lib.av_buffer_unref(&self.ptr.opaque_ref)
234+
lib.av_buffer_unref(cython.address(self.ptr.opaque_ref))
230235

231236
if v is None:
232237
return
233238
self.ptr.opaque_ref = opaque_container.add(v)
234-

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[build-system]
2-
requires = ["setuptools>61", "cython>=3,<4"]
2+
requires = ["setuptools>61", "cython>=3.1.0a1,<4"]
33

44
[project]
55
name = "av"

scripts/build

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ which ffmpeg || exit 2
2121
ffmpeg -version || exit 3
2222
echo
2323

24-
$PYAV_PIP install -U cython setuptools 2> /dev/null
24+
$PYAV_PIP install -U --pre cython setuptools 2> /dev/null
2525
"$PYAV_PYTHON" scripts/comptime.py
2626
"$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1

setup.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,15 @@ def parse_cflags(raw_flags):
177177
"library_dirs": [],
178178
}
179179

180+
IMPORT_NAME = "av"
181+
180182
loudnorm_extension = Extension(
181-
"av.filter.loudnorm",
183+
f"{IMPORT_NAME}.filter.loudnorm",
182184
sources=[
183-
"av/filter/loudnorm.pyx",
184-
"av/filter/loudnorm_impl.c",
185+
f"{IMPORT_NAME}/filter/loudnorm.py",
186+
f"{IMPORT_NAME}/filter/loudnorm_impl.c",
185187
],
186-
include_dirs=["av/filter"] + extension_extra["include_dirs"],
188+
include_dirs=[f"{IMPORT_NAME}/filter"] + extension_extra["include_dirs"],
187189
libraries=extension_extra["libraries"],
188190
library_dirs=extension_extra["library_dirs"],
189191
)
@@ -204,10 +206,14 @@ def parse_cflags(raw_flags):
204206
include_path=["include"],
205207
)
206208

207-
for dirname, dirnames, filenames in os.walk("av"):
209+
for dirname, dirnames, filenames in os.walk(IMPORT_NAME):
208210
for filename in filenames:
209211
# We are looking for Cython sources.
210-
if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx":
212+
if filename.startswith("."):
213+
continue
214+
if filename in {"__init__.py", "__main__.py", "about.py", "datasets.py"}:
215+
continue
216+
if os.path.splitext(filename)[1] not in {".pyx", ".py"}:
211217
continue
212218

213219
pyx_path = os.path.join(dirname, filename)
@@ -236,13 +242,13 @@ def parse_cflags(raw_flags):
236242
insert_enum_in_generated_files(cfile)
237243

238244

239-
package_folders = pathlib.Path("av").glob("**/")
245+
package_folders = pathlib.Path(IMPORT_NAME).glob("**/")
240246
package_data = {
241247
".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders
242248
}
243249

244250
setup(
245-
packages=find_packages(include=["av*"]),
251+
packages=find_packages(include=[f"{IMPORT_NAME}*"]),
246252
package_data=package_data,
247253
ext_modules=ext_modules,
248254
)

0 commit comments

Comments
 (0)