From 0a8f94f13b1dad011102194010c60d5692ebbaa1 Mon Sep 17 00:00:00 2001 From: Mike Boers <github@mikeboers.com> Date: Sat, 29 Sep 2018 16:39:24 -0400 Subject: [PATCH 1/3] Pull renamed_attr from metatools.deprecate. --- av/deprecation.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 av/deprecation.py diff --git a/av/deprecation.py b/av/deprecation.py new file mode 100644 index 0000000..d8cfa6f --- /dev/null +++ b/av/deprecation.py @@ -0,0 +1,59 @@ +import warnings + + +class AttributeRenamedWarning(UserWarning): + pass + + +class renamed_attr(object): + + """Proxy for renamed attributes (or methods) on classes. + Getting and setting values will be redirected to the provided name, + and warnings will be issues every time. + + E.g.:: + + >>> class Example(object): + ... + ... new_value = 'something' + ... old_value = renamed_attr('new_value') + ... + ... def new_func(self, a, b): + ... return a + b + ... + ... old_func = renamed_attr('new_func') + >>> e = Example() + >>> e.old_value = 'else' + # AttributeRenamedWarning: Example.old_value renamed to new_value + >>> e.old_func(1, 2) + # AttributeRenamedWarning: Example.old_func renamed to new_func + 3 + + """ + + def __init__(self, new_name): + self.new_name = new_name + self._old_name = None # We haven't discovered it yet. + + def old_name(self, cls): + if self._old_name is None: + for k, v in vars(cls).items(): + if v is self: + self._old_name = k + break + return self._old_name + + def __get__(self, instance, cls): + old_name = self.old_name(cls) + warnings.warn('%s.%s was renamed to %s' % ( + cls.__name__, old_name, self.new_name, + ), AttributeRenamedWarning, stacklevel=2) + return getattr(instance if instance is not None else cls, self.new_name) + + def __set__(self, instance, value): + old_name = self.old_name(instance.__class__) + warnings.warn('%s.%s was renamed to %s' % ( + instance.__class__.__name__, old_name, self.new_name, + ), AttributeRenamedWarning, stacklevel=2) + setattr(instance, self.new_name, value) + -- GitLab From c57184903a649c880542846e5b10b506e2d5c98e Mon Sep 17 00:00:00 2001 From: Mike Boers <github@mikeboers.com> Date: Mon, 1 Oct 2018 15:18:32 -0400 Subject: [PATCH 2/3] Deprecation warnings are picked up by doctest. --- av/deprecation.py | 26 ++++++++++++++++++-------- tests/common.py | 10 +++++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/av/deprecation.py b/av/deprecation.py index d8cfa6f..121ab9c 100644 --- a/av/deprecation.py +++ b/av/deprecation.py @@ -1,10 +1,17 @@ import warnings -class AttributeRenamedWarning(UserWarning): +class AttributeRenamedWarning(DeprecationWarning): pass +# DeprecationWarning is not printed by default (unless in __main__). We +# really want these to be seen, but also to use the "correct" base classes. +# So we're putting a filter in place to show our warnings. The users can +# turn them back off if they want. +warnings.filterwarnings('default', '', AttributeRenamedWarning) + + class renamed_attr(object): """Proxy for renamed attributes (or methods) on classes. @@ -22,18 +29,21 @@ class renamed_attr(object): ... return a + b ... ... old_func = renamed_attr('new_func') + >>> e = Example() - >>> e.old_value = 'else' - # AttributeRenamedWarning: Example.old_value renamed to new_value - >>> e.old_func(1, 2) - # AttributeRenamedWarning: Example.old_func renamed to new_func + + >>> e.old_value = 'else' # doctest: +ELLIPSIS + /... AttributeRenamedWarning: Example.old_value is deprecated; please use Example.new_value. ... + + >>> e.old_func(1, 2) # doctest: +ELLIPSIS + /... AttributeRenamedWarning: Example.old_func is deprecated; please use Example.new_func. ... 3 """ def __init__(self, new_name): self.new_name = new_name - self._old_name = None # We haven't discovered it yet. + self._old_name = None def old_name(self, cls): if self._old_name is None: @@ -45,14 +55,14 @@ class renamed_attr(object): def __get__(self, instance, cls): old_name = self.old_name(cls) - warnings.warn('%s.%s was renamed to %s' % ( + warnings.warn('{0}.{1} is deprecated; please use {0}.{2}.'.format( cls.__name__, old_name, self.new_name, ), AttributeRenamedWarning, stacklevel=2) return getattr(instance if instance is not None else cls, self.new_name) def __set__(self, instance, value): old_name = self.old_name(instance.__class__) - warnings.warn('%s.%s was renamed to %s' % ( + warnings.warn('{0}.{1} is deprecated; please use {0}.{2}.'.format( instance.__class__.__name__, old_name, self.new_name, ), AttributeRenamedWarning, stacklevel=2) setattr(instance, self.new_name, value) diff --git a/tests/common.py b/tests/common.py index 721063d..8a0644b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,10 +5,11 @@ from subprocess import check_call from unittest import TestCase as _Base import datetime import errno +import functools import os import sys -import functools import types +import warnings from nose.plugins.skip import SkipTest @@ -95,6 +96,13 @@ def sandboxed(*args, **kwargs): return path +# Route all warnings to stdout so that doctest will work with them. +def showwarning(message, category, filename, lineno, file=None, line=None): + msg = warnings.formatwarning(message, category, filename, lineno, line) + print(msg.rstrip()) +warnings.showwarning = showwarning + + class MethodLogger(object): def __init__(self, obj): -- GitLab From 07ff58bde32a41e9d791a2cce7b854275fc55c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= <jeremy.laine@m4x.org> Date: Mon, 1 Oct 2018 21:13:52 +0200 Subject: [PATCH 3/3] Rename Frame.to_nd_array to Frame.to_ndarray --- CHANGELOG.rst | 5 +++ av/audio/frame.pyx | 5 ++- av/video/frame.pyx | 5 ++- examples/average.py | 4 +-- examples/show_frames_opencv.py | 2 +- tests/test_audioframe.py | 58 ++++++++++++++++++++++++++++++++++ tests/test_videoframe.py | 23 ++++++++++++-- 7 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 tests/test_audioframe.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4200605..c7f68b1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,11 @@ we are using v0.x.y as our heavy development period, and will increment ``x`` to signal a major change (i.e. backwards incompatibilities) and increment ``y`` as a minor change (i.e. backwards compatible features). +v0.5.3 +------ + +- Deprecate ``AudioFrame.to_nd_array()`` and ``VideoFrame.to_nd_array()`` in + favour of ``.to_ndarray()``. v0.5.2 ------ diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index d6ebe2f..0be0576 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -1,6 +1,7 @@ from av.audio.format cimport get_audio_format from av.audio.layout cimport get_audio_layout from av.audio.plane cimport AudioPlane +from av.deprecation import renamed_attr from av.utils cimport err_check @@ -120,7 +121,7 @@ cdef class AudioFrame(Frame): def __set__(self, value): self.ptr.sample_rate = value - def to_nd_array(self, **kwargs): + def to_ndarray(self, **kwargs): """Get a numpy array of this frame. Any ``**kwargs`` are passed to :meth:`AudioFrame.reformat`. """ @@ -138,3 +139,5 @@ cdef class AudioFrame(Frame): # convert and return data return np.vstack(map(lambda x: np.frombuffer(x, dtype), self.planes)) + + to_nd_array = renamed_attr('to_ndarray') diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 9f296bd..cc3ce1d 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -1,6 +1,7 @@ from libc.stdint cimport uint8_t from av.bytesource cimport ByteSource, bytesource +from av.deprecation import renamed_attr from av.enums cimport EnumType, define_enum from av.utils cimport err_check from av.video.format cimport get_video_format, VideoFormat @@ -298,7 +299,7 @@ cdef class VideoFrame(Frame): from PIL import Image return Image.frombuffer("RGB", (self.width, self.height), self.reformat(format="rgb24", **kwargs).planes[0], "raw", "RGB", 0, 1) - def to_nd_array(self, **kwargs): + def to_ndarray(self, **kwargs): """Get a numpy array of this frame. Any ``**kwargs`` are passed to :meth:`VideoFrame.reformat`. @@ -320,6 +321,8 @@ cdef class VideoFrame(Frame): else: raise ValueError("Cannot conveniently get numpy array from %s format" % frame.format.name) + to_nd_array = renamed_attr('to_ndarray') + def to_qimage(self, **kwargs): """Get an RGB ``QImage`` of this frame. diff --git a/examples/average.py b/examples/average.py index 378db87..d32c40b 100644 --- a/examples/average.py +++ b/examples/average.py @@ -48,9 +48,9 @@ for src_path in args.path: for fi, frame in enumerate(frame_iter(video)): if sum_ is None: - sum_ = frame.to_nd_array().astype(float) + sum_ = frame.to_ndarray().astype(float) else: - sum_ += frame.to_nd_array().astype(float) + sum_ += frame.to_ndarray().astype(float) sum_ /= (fi + 1) diff --git a/examples/show_frames_opencv.py b/examples/show_frames_opencv.py index 1d5c070..c618afa 100644 --- a/examples/show_frames_opencv.py +++ b/examples/show_frames_opencv.py @@ -12,7 +12,7 @@ stream = next(s for s in video.streams if s.type == 'video') for packet in video.demux(stream): for frame in packet.decode(): # some other formats gray16be, bgr24, rgb24 - img = frame.to_nd_array(format='bgr24') + img = frame.to_ndarray(format='bgr24') cv2.imshow("Test", img) if cv2.waitKey(1) == 27: diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py new file mode 100644 index 0000000..2bdc191 --- /dev/null +++ b/tests/test_audioframe.py @@ -0,0 +1,58 @@ +import warnings + +from av import AudioFrame +from av.deprecation import AttributeRenamedWarning + +from .common import TestCase + + +class TestAudioFrameConstructors(TestCase): + + def test_null_constructor(self): + frame = AudioFrame() + self.assertEqual(frame.format.name, 's16') + self.assertEqual(frame.layout.name, 'stereo') + self.assertEqual(len(frame.planes), 0) + self.assertEqual(frame.samples, 0) + + def test_manual_s16_mono_constructor(self): + frame = AudioFrame(format='s16', layout='mono', samples=160) + self.assertEqual(frame.format.name, 's16') + self.assertEqual(frame.layout.name, 'mono') + self.assertEqual(len(frame.planes), 1) + self.assertEqual(frame.samples, 160) + + def test_manual_s16_stereo_constructor(self): + frame = AudioFrame(format='s16', layout='stereo', samples=160) + self.assertEqual(frame.format.name, 's16') + self.assertEqual(frame.layout.name, 'stereo') + self.assertEqual(len(frame.planes), 1) + self.assertEqual(frame.samples, 160) + + def test_manual_s16p_stereo_constructor(self): + frame = AudioFrame(format='s16p', layout='stereo', samples=160) + self.assertEqual(frame.format.name, 's16p') + self.assertEqual(frame.layout.name, 'stereo') + self.assertEqual(len(frame.planes), 2) + self.assertEqual(frame.samples, 160) + + +class TestAudioFrameConveniences(TestCase): + + def test_basic_to_ndarray(self): + frame = AudioFrame(format='s16p', layout='stereo', samples=160) + array = frame.to_ndarray() + self.assertEqual(array.shape, (2, 160)) + + def test_basic_to_nd_array(self): + frame = AudioFrame(format='s16p', layout='stereo', samples=160) + with warnings.catch_warnings(record=True) as recorded: + array = frame.to_nd_array() + self.assertEqual(array.shape, (2, 160)) + + # check deprecation warning + self.assertEqual(len(recorded), 1) + self.assertEqual(recorded[0].category, AttributeRenamedWarning) + self.assertEqual( + str(recorded[0].message), + 'AudioFrame.to_nd_array is deprecated; please use AudioFrame.to_ndarray.') diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 54aa73f..56b1fe3 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -1,4 +1,9 @@ -from .common import * +import warnings + +from av import VideoFrame +from av.deprecation import AttributeRenamedWarning + +from .common import fate_png, is_py3, Image, SkipTest, TestCase class TestVideoFrameConstructors(TestCase): @@ -108,11 +113,24 @@ class TestVideoFrameTransforms(TestCase): class TestVideoFrameConveniences(TestCase): + def test_basic_to_ndarray(self): + frame = VideoFrame(640, 480, 'rgb24') + array = frame.to_ndarray() + self.assertEqual(array.shape, (480, 640, 3)) + def test_basic_to_nd_array(self): frame = VideoFrame(640, 480, 'rgb24') - array = frame.to_nd_array() + with warnings.catch_warnings(record=True) as recorded: + array = frame.to_nd_array() self.assertEqual(array.shape, (480, 640, 3)) + # check deprecation warning + self.assertEqual(len(recorded), 1) + self.assertEqual(recorded[0].category, AttributeRenamedWarning) + self.assertEqual( + str(recorded[0].message), + 'VideoFrame.to_nd_array is deprecated; please use VideoFrame.to_ndarray.') + class TestVideoFrameTiming(TestCase): @@ -141,4 +159,3 @@ class TestVideoFrameReformat(TestCase): # I thought this was not allowed, but it seems to be. frame = VideoFrame(640, 480, 'yuv420p') frame2 = frame.reformat(src_colorspace=None, dst_colorspace='smpte240') - -- GitLab