From 0db382dfbe6c87e2d5f686d2889359a1d1db0d62 Mon Sep 17 00:00:00 2001 From: Valentin Zulkower Date: Tue, 19 Nov 2024 23:18:20 -0500 Subject: [PATCH] make flake8 happier --- moviepy/Effect.py | 7 +++++-- moviepy/video/VideoClip.py | 25 +++++++------------------ moviepy/video/fx/AccelDecel.py | 1 + moviepy/video/fx/BlackAndWhite.py | 1 + moviepy/video/fx/Blink.py | 1 + moviepy/video/fx/Crop.py | 1 + moviepy/video/fx/CrossFadeIn.py | 1 + moviepy/video/fx/CrossFadeOut.py | 1 + moviepy/video/fx/EvenSize.py | 1 + moviepy/video/fx/FadeIn.py | 1 + moviepy/video/fx/FadeOut.py | 1 + moviepy/video/fx/Freeze.py | 1 + moviepy/video/fx/FreezeRegion.py | 1 + moviepy/video/fx/GammaCorrection.py | 2 ++ moviepy/video/fx/HeadBlur.py | 1 + moviepy/video/fx/InvertColors.py | 1 + moviepy/video/fx/Loop.py | 1 + moviepy/video/fx/LumContrast.py | 2 ++ moviepy/video/fx/MakeLoopable.py | 1 + moviepy/video/fx/Margin.py | 2 ++ moviepy/video/fx/MaskColor.py | 1 + moviepy/video/fx/MasksAnd.py | 1 + moviepy/video/fx/MasksOr.py | 1 + moviepy/video/fx/MirrorX.py | 1 + moviepy/video/fx/MirrorY.py | 1 + moviepy/video/fx/MultiplyColor.py | 1 + moviepy/video/fx/MultiplySpeed.py | 1 + moviepy/video/fx/Painting.py | 1 + moviepy/video/fx/Resize.py | 1 + moviepy/video/fx/Rotate.py | 1 + moviepy/video/fx/Scroll.py | 1 + moviepy/video/fx/SlideIn.py | 1 + moviepy/video/fx/SlideOut.py | 1 + moviepy/video/fx/SuperSample.py | 2 ++ moviepy/video/fx/TimeMirror.py | 1 + moviepy/video/fx/TimeSymmetrize.py | 1 + moviepy/video/io/ffplay_previewer.py | 1 - moviepy/video/io/gif_writers.py | 4 +--- tests/test_doc_examples.py | 3 ++- 39 files changed, 53 insertions(+), 25 deletions(-) diff --git a/moviepy/Effect.py b/moviepy/Effect.py index 7f1fdb3dd..6f8bdf77c 100644 --- a/moviepy/Effect.py +++ b/moviepy/Effect.py @@ -1,3 +1,5 @@ +"""Defines the base class for all effects in MoviePy.""" + import copy as _copy from abc import ABCMeta, abstractmethod @@ -5,7 +7,7 @@ class Effect(metaclass=ABCMeta): - """Base abastract class for all effects in MoviePy. + """Base abstract class for all effects in MoviePy. Any new effect have to extend this base class. """ @@ -22,7 +24,8 @@ def copy(self): By using copy, we ensure we can use the same effect object multiple times while maintaining the same behavior/result. - In a way, copy make the effect himself beeing kind of indempotent.""" + In a way, copy makes the effect himself being kind of idempotent. + """ return _copy.copy(self) @abstractmethod diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index fc76fbd0e..e262ef696 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -523,8 +523,7 @@ def write_gif( @convert_masks_to_RGB @convert_parameter_to_seconds(["t"]) def show(self, t=0, with_mask=True): - """ - Splashes the frame of clip corresponding to time ``t``. + """Splashes the frame of clip corresponding to time ``t``. Parameters ---------- @@ -565,9 +564,9 @@ def show(self, t=0, with_mask=True): def preview( self, fps=15, audio=True, audio_fps=22050, audio_buffersize=3000, audio_nbytes=2 ): - """ - Displays the clip in a window, at the given frames per second (of movie) - rate. It will avoid that the clip be played faster than normal, but it + """Displays the clip in a window, at the given frames per second. + + It will avoid that the clip be played faster than normal, but it cannot avoid the clip to be played slower than normal if the computations are complex. In this case, try reducing the ``fps``. @@ -594,11 +593,9 @@ def preview( -------- >>> from moviepy import * - >>> >>> clip = VideoFileClip("media/chaplin.mp4") >>> clip.preview(fps=10, audio=False) """ - audio = audio and (self.audio is not None) audio_flag = None video_flag = None @@ -690,7 +687,6 @@ def fill_array(self, pre_array, shape=(0, 0)): shape (tuple) The desired shape of the resulting array. - """ pre_shape = pre_array.shape dx = shape[0] - pre_shape[0] @@ -1463,9 +1459,7 @@ def __init__( def break_text( width, text, font, font_size, stroke_width, align, spacing ) -> List[str]: - """ - Break text to never overflow a width - """ + """Break text to never overflow a width""" img = Image.new("RGB", (1, 1)) font_pil = ImageFont.truetype(font, font_size) draw = ImageDraw.Draw(img) @@ -1506,10 +1500,7 @@ def find_text_size( max_width=None, allow_break=False, ) -> tuple[int, int]: - """ - Find dimensions a text will occupy - return a tuple (width, height) - """ + """Find dimensions a text will occupy, return a tuple (width, height)""" img = Image.new("RGB", (1, 1)) font_pil = ImageFont.truetype(font, font_size) draw = ImageDraw.Draw(img) @@ -1559,9 +1550,7 @@ def find_optimum_font_size( height=None, allow_break=False, ): - """ - Find the best font size to fit as optimally as possible - """ + """Find the best font size to fit as optimally as possible""" max_font_size = width min_font_size = 1 diff --git a/moviepy/video/fx/AccelDecel.py b/moviepy/video/fx/AccelDecel.py index a8b146988..982daa8d3 100644 --- a/moviepy/video/fx/AccelDecel.py +++ b/moviepy/video/fx/AccelDecel.py @@ -65,6 +65,7 @@ def f2(t): return old_duration * _f((t / new_duration) ** soonness) def apply(self, clip): + """Apply the effect to the clip.""" if self.new_duration is None: self.new_duration = clip.duration diff --git a/moviepy/video/fx/BlackAndWhite.py b/moviepy/video/fx/BlackAndWhite.py index 604b778e6..bf15ad354 100644 --- a/moviepy/video/fx/BlackAndWhite.py +++ b/moviepy/video/fx/BlackAndWhite.py @@ -18,6 +18,7 @@ class BlackAndWhite(Effect): preserve_luminosity: bool = True def apply(self, clip): + """Apply the effect to the clip.""" if self.RGB is None: self.RGB = [1, 1, 1] diff --git a/moviepy/video/fx/Blink.py b/moviepy/video/fx/Blink.py index 726ce0873..2274c02d1 100644 --- a/moviepy/video/fx/Blink.py +++ b/moviepy/video/fx/Blink.py @@ -15,6 +15,7 @@ class Blink(Effect): duration_off: float def apply(self, clip): + """Apply the effect to the clip.""" if clip.mask is None: clip = clip.with_add_mask() diff --git a/moviepy/video/fx/Crop.py b/moviepy/video/fx/Crop.py index cb564acda..81e7f5025 100644 --- a/moviepy/video/fx/Crop.py +++ b/moviepy/video/fx/Crop.py @@ -45,6 +45,7 @@ class Crop(Effect): """ def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if self.width and self.x1 is not None: self.x2 = self.x1 + self.width elif self.width and self.x2 is not None: diff --git a/moviepy/video/fx/CrossFadeIn.py b/moviepy/video/fx/CrossFadeIn.py index d8881bd63..09d179c68 100644 --- a/moviepy/video/fx/CrossFadeIn.py +++ b/moviepy/video/fx/CrossFadeIn.py @@ -14,6 +14,7 @@ class CrossFadeIn(Effect): duration: float def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if clip.duration is None: raise ValueError("Attribute 'duration' not set") diff --git a/moviepy/video/fx/CrossFadeOut.py b/moviepy/video/fx/CrossFadeOut.py index 038413093..5076240ad 100644 --- a/moviepy/video/fx/CrossFadeOut.py +++ b/moviepy/video/fx/CrossFadeOut.py @@ -14,6 +14,7 @@ class CrossFadeOut(Effect): duration: float def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if clip.duration is None: raise ValueError("Attribute 'duration' not set") diff --git a/moviepy/video/fx/EvenSize.py b/moviepy/video/fx/EvenSize.py index e024c8310..bea2a5596 100644 --- a/moviepy/video/fx/EvenSize.py +++ b/moviepy/video/fx/EvenSize.py @@ -9,6 +9,7 @@ class EvenSize(Effect): """Crops the clip to make dimensions even.""" def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" w, h = clip.size w_even = w % 2 == 0 h_even = h % 2 == 0 diff --git a/moviepy/video/fx/FadeIn.py b/moviepy/video/fx/FadeIn.py index 9a5746b42..2078fcbcf 100644 --- a/moviepy/video/fx/FadeIn.py +++ b/moviepy/video/fx/FadeIn.py @@ -20,6 +20,7 @@ class FadeIn(Effect): initial_color: list = None def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if self.initial_color is None: self.initial_color = 0 if clip.is_mask else [0, 0, 0] diff --git a/moviepy/video/fx/FadeOut.py b/moviepy/video/fx/FadeOut.py index 75b47ee8f..c0f3dcbfa 100644 --- a/moviepy/video/fx/FadeOut.py +++ b/moviepy/video/fx/FadeOut.py @@ -20,6 +20,7 @@ class FadeOut(Effect): final_color: list = None def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if clip.duration is None: raise ValueError("Attribute 'duration' not set") diff --git a/moviepy/video/fx/Freeze.py b/moviepy/video/fx/Freeze.py index ebaf75529..40dc3c9f9 100644 --- a/moviepy/video/fx/Freeze.py +++ b/moviepy/video/fx/Freeze.py @@ -23,6 +23,7 @@ class Freeze(Effect): padding_end: float = 0 def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if clip.duration is None: raise ValueError("Attribute 'duration' not set") diff --git a/moviepy/video/fx/FreezeRegion.py b/moviepy/video/fx/FreezeRegion.py index 62c6112f4..dd9b71737 100644 --- a/moviepy/video/fx/FreezeRegion.py +++ b/moviepy/video/fx/FreezeRegion.py @@ -40,6 +40,7 @@ class FreezeRegion(Effect): mask: Clip = None def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if self.region is not None: x1, y1, _x2, _y2 = self.region freeze = ( diff --git a/moviepy/video/fx/GammaCorrection.py b/moviepy/video/fx/GammaCorrection.py index df81a4f5d..1582e26bf 100644 --- a/moviepy/video/fx/GammaCorrection.py +++ b/moviepy/video/fx/GammaCorrection.py @@ -11,6 +11,8 @@ class GammaCorrection(Effect): gamma: float def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" + def filter(im): corrected = 255 * (1.0 * im / 255) ** self.gamma return corrected.astype("uint8") diff --git a/moviepy/video/fx/HeadBlur.py b/moviepy/video/fx/HeadBlur.py index f22fc399e..83aec1a76 100644 --- a/moviepy/video/fx/HeadBlur.py +++ b/moviepy/video/fx/HeadBlur.py @@ -21,6 +21,7 @@ class HeadBlur(Effect): intensity: float = None def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if self.intensity is None: self.intensity = int(2 * self.radius / 3) diff --git a/moviepy/video/fx/InvertColors.py b/moviepy/video/fx/InvertColors.py index 3df536fef..cf12a72b2 100644 --- a/moviepy/video/fx/InvertColors.py +++ b/moviepy/video/fx/InvertColors.py @@ -13,5 +13,6 @@ class InvertColors(Effect): """ def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" maxi = 1.0 if clip.is_mask else 255 return clip.image_transform(lambda f: maxi - f) diff --git a/moviepy/video/fx/Loop.py b/moviepy/video/fx/Loop.py index e283569f8..c8c8c7797 100644 --- a/moviepy/video/fx/Loop.py +++ b/moviepy/video/fx/Loop.py @@ -25,6 +25,7 @@ class Loop(Effect): duration: float = None def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if clip.duration is None: raise ValueError("Attribute 'duration' not set") diff --git a/moviepy/video/fx/LumContrast.py b/moviepy/video/fx/LumContrast.py index 4ada93b89..cfba9a60c 100644 --- a/moviepy/video/fx/LumContrast.py +++ b/moviepy/video/fx/LumContrast.py @@ -13,6 +13,8 @@ class LumContrast(Effect): contrast_threshold: float = 127 def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" + def image_filter(im): im = 1.0 * im # float conversion corrected = ( diff --git a/moviepy/video/fx/MakeLoopable.py b/moviepy/video/fx/MakeLoopable.py index c8ada8348..5632b7c1a 100644 --- a/moviepy/video/fx/MakeLoopable.py +++ b/moviepy/video/fx/MakeLoopable.py @@ -21,6 +21,7 @@ class MakeLoopable(Effect): overlap_duration: float def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" clip2 = clip.with_effects([CrossFadeIn(self.overlap_duration)]).with_start( clip.duration - self.overlap_duration ) diff --git a/moviepy/video/fx/Margin.py b/moviepy/video/fx/Margin.py index 91f463280..77c5389c2 100644 --- a/moviepy/video/fx/Margin.py +++ b/moviepy/video/fx/Margin.py @@ -46,6 +46,7 @@ class Margin(Effect): opacity: float = 1.0 def add_margin(self, clip: Clip): + """Add margins to the clip.""" if (self.opacity != 1.0) and (clip.mask is None) and not (clip.is_mask): clip = clip.with_add_mask() @@ -79,6 +80,7 @@ def filter(get_frame, t): return clip.transform(filter) def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" # We apply once on clip and once on mask if we have one clip = self.add_margin(clip=clip) diff --git a/moviepy/video/fx/MaskColor.py b/moviepy/video/fx/MaskColor.py index cdd24aecd..2cb92bd4a 100644 --- a/moviepy/video/fx/MaskColor.py +++ b/moviepy/video/fx/MaskColor.py @@ -26,6 +26,7 @@ class MaskColor(Effect): stiffness: float = 1 def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" color = np.array(self.color) def hill(x): diff --git a/moviepy/video/fx/MasksAnd.py b/moviepy/video/fx/MasksAnd.py index bd7b4e3b4..a67d8d271 100644 --- a/moviepy/video/fx/MasksAnd.py +++ b/moviepy/video/fx/MasksAnd.py @@ -33,6 +33,7 @@ class MasksAnd(Effect): other_clip: Union[Clip, np.ndarray] def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" # to ensure that 'and' of two ImageClips will be an ImageClip if isinstance(self.other_clip, ImageClip): self.other_clip = self.other_clip.img diff --git a/moviepy/video/fx/MasksOr.py b/moviepy/video/fx/MasksOr.py index d1ce74ab1..7d215c4e1 100644 --- a/moviepy/video/fx/MasksOr.py +++ b/moviepy/video/fx/MasksOr.py @@ -33,6 +33,7 @@ class MasksOr(Effect): other_clip: Union[Clip, np.ndarray] def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" # to ensure that 'or' of two ImageClips will be an ImageClip if isinstance(self.other_clip, ImageClip): self.other_clip = self.other_clip.img diff --git a/moviepy/video/fx/MirrorX.py b/moviepy/video/fx/MirrorX.py index becbe1799..d623610da 100644 --- a/moviepy/video/fx/MirrorX.py +++ b/moviepy/video/fx/MirrorX.py @@ -12,4 +12,5 @@ class MirrorX(Effect): apply_to: Union[List, str] = "mask" def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" return clip.image_transform(lambda img: img[:, ::-1], apply_to=self.apply_to) diff --git a/moviepy/video/fx/MirrorY.py b/moviepy/video/fx/MirrorY.py index e96c9b77e..8d8d5b0de 100644 --- a/moviepy/video/fx/MirrorY.py +++ b/moviepy/video/fx/MirrorY.py @@ -12,4 +12,5 @@ class MirrorY(Effect): apply_to: Union[List, str] = "mask" def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" return clip.image_transform(lambda img: img[::-1], apply_to=self.apply_to) diff --git a/moviepy/video/fx/MultiplyColor.py b/moviepy/video/fx/MultiplyColor.py index 4976f8dbf..6a3e1b70b 100644 --- a/moviepy/video/fx/MultiplyColor.py +++ b/moviepy/video/fx/MultiplyColor.py @@ -17,6 +17,7 @@ class MultiplyColor(Effect): factor: float def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" return clip.image_transform( lambda frame: np.minimum(255, (self.factor * frame)).astype("uint8") ) diff --git a/moviepy/video/fx/MultiplySpeed.py b/moviepy/video/fx/MultiplySpeed.py index 72c9558e8..68a99b311 100644 --- a/moviepy/video/fx/MultiplySpeed.py +++ b/moviepy/video/fx/MultiplySpeed.py @@ -17,6 +17,7 @@ class MultiplySpeed(Effect): final_duration: float = None def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if self.final_duration: self.factor = 1.0 * clip.duration / self.final_duration diff --git a/moviepy/video/fx/Painting.py b/moviepy/video/fx/Painting.py index 668016628..30484c6f3 100644 --- a/moviepy/video/fx/Painting.py +++ b/moviepy/video/fx/Painting.py @@ -57,6 +57,7 @@ def to_painting(self, np_image, saturation=1.4, black=0.006): return painting def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" return clip.image_transform( lambda im: self.to_painting(im, self.saturation, self.black) ) diff --git a/moviepy/video/fx/Resize.py b/moviepy/video/fx/Resize.py index 67ca0a7e4..a7baa5d1d 100644 --- a/moviepy/video/fx/Resize.py +++ b/moviepy/video/fx/Resize.py @@ -50,6 +50,7 @@ def resizer(self, pic, new_size): return np.array(resized_pil) def apply(self, clip): + """Apply the effect to the clip.""" w, h = clip.size if self.new_size is not None: diff --git a/moviepy/video/fx/Rotate.py b/moviepy/video/fx/Rotate.py index e6c9f2af1..b19c5e4e4 100644 --- a/moviepy/video/fx/Rotate.py +++ b/moviepy/video/fx/Rotate.py @@ -58,6 +58,7 @@ class Rotate(Effect): bg_color: tuple = None def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" try: resample = { "bilinear": Image.BILINEAR, diff --git a/moviepy/video/fx/Scroll.py b/moviepy/video/fx/Scroll.py index 555c15a0f..e7b17e2f2 100644 --- a/moviepy/video/fx/Scroll.py +++ b/moviepy/video/fx/Scroll.py @@ -38,6 +38,7 @@ def __init__( self.apply_to = apply_to def apply(self, clip): + """Apply the effect to the clip.""" if self.h is None: self.h = clip.h diff --git a/moviepy/video/fx/SlideIn.py b/moviepy/video/fx/SlideIn.py index 0462c70a4..56277e097 100644 --- a/moviepy/video/fx/SlideIn.py +++ b/moviepy/video/fx/SlideIn.py @@ -46,6 +46,7 @@ class SlideIn(Effect): side: str def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" w, h = clip.size pos_dict = { "left": lambda t: (min(0, w * (t / self.duration - 1)), "center"), diff --git a/moviepy/video/fx/SlideOut.py b/moviepy/video/fx/SlideOut.py index d0030879e..49e4c2a44 100644 --- a/moviepy/video/fx/SlideOut.py +++ b/moviepy/video/fx/SlideOut.py @@ -46,6 +46,7 @@ class SlideOut(Effect): side: str def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if clip.duration is None: raise ValueError("Attribute 'duration' not set") diff --git a/moviepy/video/fx/SuperSample.py b/moviepy/video/fx/SuperSample.py index f3b642136..9c18d81c9 100644 --- a/moviepy/video/fx/SuperSample.py +++ b/moviepy/video/fx/SuperSample.py @@ -16,6 +16,8 @@ class SuperSample(Effect): n_frames: int def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" + def filter(get_frame, t): timings = np.linspace(t - self.d, t + self.d, self.n_frames) frame_average = np.mean( diff --git a/moviepy/video/fx/TimeMirror.py b/moviepy/video/fx/TimeMirror.py index 6ea277857..d90238e93 100644 --- a/moviepy/video/fx/TimeMirror.py +++ b/moviepy/video/fx/TimeMirror.py @@ -13,6 +13,7 @@ class TimeMirror(Effect): """ def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if clip.duration is None: raise ValueError("Attribute 'duration' not set") diff --git a/moviepy/video/fx/TimeSymmetrize.py b/moviepy/video/fx/TimeSymmetrize.py index adf9be9de..c85f4f8a7 100644 --- a/moviepy/video/fx/TimeSymmetrize.py +++ b/moviepy/video/fx/TimeSymmetrize.py @@ -15,6 +15,7 @@ class TimeSymmetrize(Effect): """ def apply(self, clip: Clip) -> Clip: + """Apply the effect to the clip.""" if clip.duration is None: raise ValueError("Attribute 'duration' not set") diff --git a/moviepy/video/io/ffplay_previewer.py b/moviepy/video/io/ffplay_previewer.py index 23bfb685b..28f3569ab 100644 --- a/moviepy/video/io/ffplay_previewer.py +++ b/moviepy/video/io/ffplay_previewer.py @@ -120,7 +120,6 @@ def ffplay_preview_video( A thread event that video will set after first frame has been shown. If not provided, we simply ignore """ - with FFPLAY_VideoPreviewer(clip.size, fps, pixel_format) as previewer: first_frame = True for t, frame in clip.iter_frames(with_times=True, fps=fps, dtype="uint8"): diff --git a/moviepy/video/io/gif_writers.py b/moviepy/video/io/gif_writers.py index 57a69a33e..ceb913d7a 100644 --- a/moviepy/video/io/gif_writers.py +++ b/moviepy/video/io/gif_writers.py @@ -9,9 +9,7 @@ @requires_duration @use_clip_fps_by_default def write_gif_with_imageio(clip, filename, fps=None, loop=0, logger="bar"): - """ - Writes the gif with the Python library ImageIO (calls FreeImage). - """ + """Writes the gif with the Python library ImageIO (calls FreeImage).""" logger = proglog.default_bar_logger(logger) with iio.imopen(filename, "w", plugin="pillow") as writer: diff --git a/tests/test_doc_examples.py b/tests/test_doc_examples.py index bd77317f2..2e5ac5c2f 100644 --- a/tests/test_doc_examples.py +++ b/tests/test_doc_examples.py @@ -1,5 +1,6 @@ """Try to run all the documentation examples with runpy and check they dont raise -exception""" +exceptions. +""" import os import pathlib