Skip to content

Commit a55d5c8

Browse files
GH-12: Add docstrings to functions, methods and classes (GH-25)
* Cache the `arange` function * Return the class after registering to factory * Add the useful classes to the `__all__` * Refactor the factory method name
1 parent 99a4833 commit a55d5c8

File tree

5 files changed

+77
-27
lines changed

5 files changed

+77
-27
lines changed

thumbnails/__init__.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from PIL import Image
88

9-
from .formatter import ThumbnailFactory
9+
from .formatter import FormatterFactory
1010
from .formatter import ThumbnailFormat
1111
from .thumbnails import Thumbnails
1212
from .thumbnails import arange
@@ -18,15 +18,24 @@
1818

1919

2020
def register_format(typename):
21-
def _registrator(cls: ThumbnailFormat):
21+
"""Register a new thumbnail format to the factory."""
22+
23+
def _registrator(cls):
24+
if not issubclass(cls, ThumbnailFormat):
25+
raise ValueError("Thumbnail format must implement"
26+
"the ThumbnailFormat interface.")
27+
2228
cls.extension = typename
23-
ThumbnailFactory.thumbnails[typename] = cls
29+
FormatterFactory.thumbnails[typename] = cls
30+
return cls
2431

2532
return _registrator
2633

2734

2835
@register_format("vtt")
2936
class VTT(ThumbnailFormat):
37+
"""Implements the methods for generating thumbnails in the WebVTT format."""
38+
3039
def __init__(self, video):
3140
super().__init__(video)
3241
self._master_name = self.filename + ".png"
@@ -64,6 +73,8 @@ def _format_time(secs):
6473

6574
@register_format("json")
6675
class JSON(ThumbnailFormat):
76+
"""Implements the methods for generating thumbnails in the JSON format."""
77+
6778
def __init__(self, video):
6879
super().__init__(video)
6980
self._outdir = "outdir" # temp dirname
@@ -92,4 +103,11 @@ def generate(self):
92103

93104

94105
__version__ = "v1.0"
95-
__all__ = (Thumbnails,)
106+
__all__ = (
107+
FormatterFactory,
108+
register_format,
109+
ThumbnailFormat,
110+
Thumbnails,
111+
JSON,
112+
VTT,
113+
)

thumbnails/__main__.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,22 @@
88
from . import DEFAULT_BASEPATH
99
from . import DEFAULT_COMPRESS
1010
from . import DEFAULT_INTERVAL
11-
from . import ThumbnailFactory
11+
from . import FormatterFactory
1212
from . import Thumbnails
1313
from . import __version__
1414

1515

1616
def worker(video, as_):
17+
"""Generate thumbnails for a single video."""
1718
video.extract_frames()
18-
formatter = ThumbnailFactory.get_formatter(as_, video)
19+
formatter = FormatterFactory.create_formatter(as_, video)
1920
formatter.prepare_thumbnails()
2021
formatter.generate()
2122

2223

2324
class _ThumbnailsCLI(click.Command):
25+
"""This class overrides the usages section of the help message."""
26+
2427
def format_usage(self, ctx, formatter):
2528
usages = (
2629
"[OPTIONS] INPUT_DIR OUTPUT_DIR",
@@ -32,15 +35,15 @@ def format_usage(self, ctx, formatter):
3235

3336
@click.command(cls=_ThumbnailsCLI)
3437
@click.option("--as", "-F", default=DEFAULT_AS, help="Output format. Default is %s." % DEFAULT_AS,
35-
type=click.Choice(ThumbnailFactory.thumbnails.keys(), case_sensitive=False))
38+
type=click.Choice(FormatterFactory.thumbnails.keys(), case_sensitive=False))
3639
@click.option("--compress", "-C", default=DEFAULT_COMPRESS, help="The image scale coefficient. A number from 0 to 1.")
3740
@click.option("--interval", "-I", default=DEFAULT_INTERVAL, help="The interval between neighbor thumbnails in seconds.")
3841
@click.option("--basepath", "-B", default=DEFAULT_BASEPATH, help="The prefix of the thumbnails path can be customized.")
3942
@click.argument("inputs", required=True, type=click.Path(), nargs=-1)
4043
@click.argument("output", required=True, type=click.Path(), nargs=1)
4144
@click.version_option(__version__)
4245
def thumbnails_cli(compress, interval, basepath, inputs, output, **kwargs):
43-
"""TODO: Add more description about particular usages."""
46+
"""TODO: This section will be completed after fixing the issue #26."""
4447
as_ = kwargs.pop("as")
4548
output_is_directory = all((len(inputs) > 1, *map(os.path.isfile, inputs))) or os.path.isdir(inputs[0])
4649

@@ -59,10 +62,5 @@ def thumbnails_cli(compress, interval, basepath, inputs, output, **kwargs):
5962
executor.map(functools.partial(worker, as_=as_), videos)
6063

6164

62-
# @click.confirmation_option("--overwrite", "-y", prompt="Are you sure you want to overwrite the existing output files?")
63-
# def overwrite():
64-
# print("overwritten")
65-
66-
6765
if __name__ == "__main__":
6866
thumbnails_cli()

thumbnails/ffmpeg.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010

1111
class _FFMpeg:
12+
"""This class is used to parse the metadata of a video file."""
13+
1214
def __init__(self, filename):
1315
duration, self.size = self._parse_metadata(filename)
1416
self.duration = int(duration + 1)
@@ -27,22 +29,28 @@ def _cross_platform_popen_params(bufsize=100000):
2729

2830
@staticmethod
2931
def _parse_duration(stdout):
32+
"""Parse the duration of a video from stdout."""
3033
duration_regex = r"duration[^\n]+([0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9])"
3134
time = re.search(duration_regex, stdout, re.M | re.I).group(1)
3235
time = (float(part.replace(",", ".")) for part in time.split(":"))
3336
return sum(mult * part for mult, part in zip((3600, 60, 1), time))
3437

3538
@staticmethod
3639
def _parse_size(stdout):
40+
"""Parse the size of a video from stdout."""
3741
size_regex = r"\s(\d+)x(\d+)[,\s]"
3842
match_size = re.search(size_regex, stdout, re.M)
3943
return tuple(map(int, match_size.groups()))
4044

4145
def _parse_metadata(self, filename):
46+
"""Parse the metadata of a video file."""
4247
meta = immeta(filename)
4348
duration, size = meta.get("duration"), meta.get("size")
4449

4550
if not all((duration, size)):
51+
# Parse the metadata of the video formats
52+
# that are not supported by imageio.
53+
4654
cmd = (ffmpeg_bin, "-hide_banner", "-i", filename)
4755

4856
popen_params = self._cross_platform_popen_params()

thumbnails/formatter.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
class ThumbnailFormat:
2+
"""The interface of the thumbnails' final output format generator."""
3+
24
extension = None
35

46
def __init__(self, video):
57
self.video = video
68

79
def __getattr__(self, item):
10+
"""Delegate all other attributes to the video."""
811
return getattr(self.video, item)
912

1013
@property
@@ -20,12 +23,15 @@ def generate(self):
2023
raise NotImplementedError
2124

2225

23-
class ThumbnailFactory:
26+
class FormatterFactory:
27+
"""A factory for creating thumbnail formatter."""
28+
2429
thumbnails = {}
2530

2631
@classmethod
27-
def get_formatter(cls, typename, *args, **kwargs) -> ThumbnailFormat:
32+
def create_formatter(cls, typename, *args, **kwargs) -> ThumbnailFormat:
33+
"""Create a new thumbnail formatter by the given typename."""
2834
try:
2935
return cls.thumbnails[typename](*args, **kwargs)
3036
except KeyError:
31-
raise ValueError("Thumbnail type '%s' is not supported." % typename)
37+
raise ValueError("Thumbnail format '%s' is not supported." % typename)

thumbnails/thumbnails.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import concurrent.futures
2+
import functools
23
import glob
34
import math
45
import os
@@ -13,7 +14,10 @@
1314
ffmpeg_bin = get_ffmpeg_exe()
1415

1516

17+
@functools.cache
1618
def arange(start, stop, step):
19+
"""Roughly equivalent to numpy.arange."""
20+
1721
def _generator():
1822
nonlocal start
1923
while start < stop:
@@ -24,10 +28,9 @@ def _generator():
2428

2529

2630
class _ThumbnailMixin:
27-
def __init__(self, size):
28-
self._w = None
29-
self._h = None
31+
"""This mixin class is used to optimally calculate the size of a thumbnail frame."""
3032

33+
def __init__(self, size):
3134
width, height = size
3235
_min_width = 300
3336
_min_height = math.ceil(_min_width * height / width)
@@ -39,22 +42,23 @@ def __init__(self, size):
3942

4043
@property
4144
def compress(self):
45+
"""Defines an interface for the compress property."""
4246
raise NotImplementedError
4347

44-
@property
48+
@functools.cached_property
4549
def width(self):
46-
if not self._w:
47-
self._w = max(self._min_width, self._width * self.compress)
48-
return self._w
50+
"""Calculates and caches the width."""
51+
return max(self._min_width, self._width * self.compress)
4952

50-
@property
53+
@functools.cached_property
5154
def height(self):
52-
if not self._h:
53-
self._h = max(self._min_height, self._height * self.compress)
54-
return self._h
55+
"""Calculates and caches the height."""
56+
return max(self._min_height, self._height * self.compress)
5557

5658

5759
class Thumbnails(_ThumbnailMixin, _FFMpeg):
60+
"""The main class for processing the thumbnail generation of a video."""
61+
5862
def __init__(self, filename, compress, interval, basepath):
5963
self.__compress = float(compress)
6064
self.__interval = float(interval)
@@ -83,12 +87,14 @@ def basepath(self):
8387

8488
@staticmethod
8589
def calc_columns(frames_count, width, height):
90+
"""Calculates an optimal number of columns for 16:9 aspect ratio."""
8691
ratio = 16 / 9
8792
for col in range(1, frames_count):
8893
if (col * width) / (frames_count // col * height) > ratio:
8994
return col
9095

9196
def _extract_frame(self, start_time):
97+
"""Extracts a single frame from the video by the given time."""
9298
_input_file = self.filename
9399
_timestamp = str(timedelta(seconds=start_time))
94100
_output_file = "%s/%s-%s.png" % (self.tempdir.name, _timestamp, self.filename)
@@ -106,11 +112,25 @@ def _extract_frame(self, start_time):
106112
subprocess.Popen(cmd).wait()
107113

108114
def extract_frames(self):
115+
"""Extracts the frames from the video by given intervals."""
109116
_intervals = arange(0, self.duration, self.interval)
110117
with concurrent.futures.ThreadPoolExecutor() as executor:
111118
executor.map(self._extract_frame, _intervals)
112119

113120
def thumbnails(self, master_size=False):
121+
"""This generator function yields a thumbnail on each iteration.
122+
123+
A thumbnail is a tuple of data describing the current frame.
124+
The thumbnail structure is (frame, start, end, x, y) where:
125+
- frame: Filename of the current frame in temp-files.
126+
- start: The start point of the time range the frame belongs to.
127+
- end: The end point of the time range the frame belongs to.
128+
- x: The X coordinate of the frame in the final image.
129+
- y: The Y coordinate of the frame in the final image.
130+
131+
:param master_size:
132+
If True, the master size will be yielded on the first iteration. Default is False.
133+
"""
114134
line, column = 0, 0
115135
frames = sorted(glob.glob(self.tempdir.name + os.sep + "*.png"))
116136
frames_count = len(arange(0, self.duration, self.interval))

0 commit comments

Comments
 (0)