Skip to content

Commit 31284d4

Browse files
authored
Merge pull request #5 from Supporterino/feat/exclude
feat: ✨ add a regex exclude option
2 parents 6e22cda + 9e6a4e1 commit 31284d4

File tree

6 files changed

+98
-53
lines changed

6 files changed

+98
-53
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.2.0] - 2024-XX-XX
9+
10+
### Added
11+
12+
- New `--exclude` option to allow filtering out files based on a regex pattern.
13+
- Example: `photo-organizer /path/to/source /path/to/target --exclude "^ignore|\.tmp$"`
14+
- This helps exclude unwanted files like temporary files or specific naming patterns.
15+
- Updated documentation to reflect the new feature.
16+
- Added unit tests for the `--exclude` functionality.
17+
818
## [1.1.1] - 2024-06-25
919

1020
### Changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# Photo Organizer
22
[![Tests](https://github.com/Supporterino/photo-organizer/actions/workflows/python-package.yml/badge.svg)](https://github.com/Supporterino/photo-organizer/actions/workflows/python-package.yml)[![Upload Release](https://github.com/Supporterino/photo-organizer/actions/workflows/python-publish.yml/badge.svg)](https://github.com/Supporterino/photo-organizer/actions/workflows/python-publish.yml)[![PyPI version](https://badge.fury.io/py/photo-organizer.svg)](https://badge.fury.io/py/photo-organizer)
33

4-
Photo Organizer is a Python script that sorts photos from a source directory into a target directory based on their creation date. The script can organize photos into year, month, and optionally day subfolders. It also supports copying or moving files, recursive directory traversal, and filtering by file extensions.
4+
Photo Organizer is a Python script that sorts photos from a source directory into a target directory based on their creation date. The script can organize photos into year, month, and optionally day subfolders. It also supports copying or moving files, recursive directory traversal, filtering by file extensions, and excluding files using regex patterns.
55

66
## Features
77

88
* Organize photos into year/month/day folders based on creation date
99
* Move or copy files from the source to the target directory
1010
* Recursively traverse directories
1111
* Filter files by specified extensions
12+
* Exclude files from processing using regex patterns
1213
* Verbose logging for detailed information
1314
* Flexible folder structure with optional top-level year-month folders.
1415

@@ -29,7 +30,7 @@ pip install photo-organizer
2930
### Running the Script
3031

3132
```bash
32-
photo-organizer [-h] [-r] [-d] [-e [ENDINGS [ENDINGS ...]]] [-v] [-c] [--no-year] source target
33+
photo-organizer [-h] [-r] [-d] [-e [ENDINGS [ENDINGS ...]]] [-v] [-c] [--no-year] [--exclude EXCLUDE_PATTERN] source target
3334
```
3435
3536
### Arguments
@@ -42,6 +43,7 @@ Options
4243
* `-r`, `--recursive`: Sort photos recursively from the source directory
4344
* `-d`, `--daily`: Organize photos into daily folders (year/month/day)
4445
* `-e`, `--endings`: Specify file endings/extensions to copy (e.g., .jpg .png). If not specified, all files are included
46+
* `--exclude`: Provide a regex pattern to exclude matching files from being processed
4547
* `-v`, `--verbose`: Enable verbose logging
4648
* `-c`, `--copy`: Copy files instead of moving them
4749
* `--no-year`: Do not place month folders inside a year folder; place them top-level with the name format YEAR-MONTH
@@ -63,6 +65,11 @@ Copy only .jpg and .png files:
6365
photo_organizer /path/to/source /path/to/target -e .jpg .png -c
6466
```
6567
68+
Exclude files matching a specific regex pattern:
69+
```bash
70+
photo-organizer /path/to/source /path/to/target --exclude "^ignore|\.tmp$"
71+
```
72+
6673
Enable verbose logging:
6774
```bash
6875
photo_organizer /path/to/source /path/to/target -v
@@ -78,7 +85,7 @@ Combine options to copy .jpg and .png files recursively into daily folders with
7885
photo-organizer -r -d -e .jpg .png -v -c /path/to/source /path/to/target
7986
```
8087
81-
## Dwevelopment
88+
## Development
8289
8390
To contribute to this project, follow these steps:
8491

photo_organizer/main.py

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,41 @@
22

33
import argparse
44
import os
5+
import re
56
import shutil
67
import datetime
78
import logging
89
import filecmp
910

1011

11-
def list_files(source, recursive=False, file_endings=None):
12+
def list_files(source, recursive=False, file_endings=None, exclude_pattern=None):
1213
"""
13-
List all files in the source directory.
14+
List all files in the source directory, optionally filtering by file extension and excluding files by regex.
1415
1516
Parameters:
1617
source (str): The source directory path.
1718
recursive (bool): If True, list files recursively. Default is False.
18-
file_endings (list): List of file endings/extensions to include. Default is None.
19+
file_endings (list): List of file extensions to include (e.g., ['.jpg', '.png']). Default is None.
20+
exclude_pattern (str): A regex pattern to exclude matching files. Default is None.
1921
2022
Returns:
21-
list: A list of file paths.
23+
list: A list of file paths that meet the criteria.
2224
"""
2325
file_list = []
26+
pattern = re.compile(exclude_pattern) if exclude_pattern else None
27+
2428
if recursive:
2529
for root, dirs, files in os.walk(source):
2630
for file in files:
27-
if not file_endings or file.lower().endswith(tuple(file_endings)):
31+
if (not file_endings or file.lower().endswith(tuple(file_endings))) and (not pattern or not pattern.search(file)):
2832
file_list.append(os.path.join(root, file))
2933
else:
3034
with os.scandir(source) as entries:
3135
for entry in entries:
32-
if entry.is_file() and (
33-
not file_endings or entry.name.lower().endswith(tuple(file_endings))
34-
):
36+
if entry.is_file() and (not file_endings or entry.name.lower().endswith(tuple(file_endings))) and (not pattern or not pattern.search(entry.name)):
3537
file_list.append(entry.path)
36-
logging.debug(f"Listed {len(file_list)} files from {source}")
38+
39+
logging.debug(f"Listed {len(file_list)} files from {source}, excluding pattern: {exclude_pattern}")
3740
return file_list
3841

3942

@@ -95,45 +98,24 @@ def configure_logging(verbose):
9598

9699
def parse_arguments():
97100
"""
98-
Parse command line arguments.
101+
Parse command-line arguments for the photo organizer.
99102
100103
Returns:
101-
Namespace: Parsed command line arguments.
104+
argparse.Namespace: Parsed command-line arguments.
102105
"""
103106
parser = argparse.ArgumentParser(
104107
description="Sort photos from source to target directory."
105108
)
106109
parser.add_argument("source", type=str, help="The source directory")
107110
parser.add_argument("target", type=str, help="The target directory")
108-
parser.add_argument(
109-
"-r", "--recursive", action="store_true", help="Sort photos recursively"
110-
)
111-
parser.add_argument(
112-
"-d",
113-
"--daily",
114-
action="store_true",
115-
default=False,
116-
help="Folder structure with daily folders",
117-
)
118-
parser.add_argument(
119-
"-e",
120-
"--endings",
121-
type=str,
122-
nargs="*",
123-
help="File endings/extensions to copy (e.g., .jpg .png)",
124-
)
125-
parser.add_argument(
126-
"-v", "--verbose", action="store_true", help="Enable verbose logging"
127-
)
128-
parser.add_argument(
129-
"-c", "--copy", action="store_true", help="Copy files instead of moving them"
130-
)
131-
parser.add_argument(
132-
"--no-year",
133-
action="store_true",
134-
help="Do not place month folders inside a year folder",
135-
)
136-
111+
parser.add_argument("-r", "--recursive", action="store_true", help="Sort photos recursively")
112+
parser.add_argument("-d", "--daily", action="store_true", default=False, help="Folder structure with daily folders")
113+
parser.add_argument("-e", "--endings", type=str, nargs="*", help="File endings/extensions to copy (e.g., .jpg .png)")
114+
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
115+
parser.add_argument("-c", "--copy", action="store_true", help="Copy files instead of moving them")
116+
parser.add_argument("--no-year", action="store_true", help="Do not place month folders inside a year folder")
117+
parser.add_argument("--exclude", type=str, help="Regex pattern to exclude files from processing")
118+
137119
return parser.parse_args()
138120

139121

@@ -192,24 +174,27 @@ def organize_files(args, files):
192174

193175

194176
def main():
177+
"""
178+
Main entry point for the photo organizer script. Parses arguments, configures logging, and processes files.
179+
"""
195180
# Parse the arguments
196181
args = parse_arguments()
197-
182+
198183
# Configure logging
199184
configure_logging(args.verbose)
200-
185+
201186
logging.info("Starting file sorting process")
202-
187+
203188
# Ensure the source directory exists
204189
if not os.path.exists(args.source):
205190
logging.error(f"Source directory '{args.source}' does not exist.")
206191
return
207-
192+
208193
# Ensure the target directory exists
209194
ensure_directory_exists(args.target)
210-
211-
# List all files in the source directory
212-
files = list_files(args.source, args.recursive, args.endings)
213-
195+
196+
# List all files in the source directory, applying the exclusion filter
197+
files = list_files(args.source, args.recursive, args.endings, args.exclude)
198+
214199
# Organize files by moving or copying them to the target directory
215200
organize_files(args, files)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "photo_organizer"
7-
version = "1.1.1"
7+
version = "1.2.0"
88
description = "A script to organize photos by creation date into year/month/day folders."
99
readme = "README.md"
1010
authors = [

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
setup(
66
name='photo_organizer',
7-
version='1.1.1',
7+
version='1.2.0',
88
packages=find_packages(),
99
entry_points={
1010
'console_scripts': [

tests/test_main.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,49 @@ def setup_target_directory():
3232
with tempfile.TemporaryDirectory() as target_dir:
3333
yield target_dir
3434

35+
def create_test_files(base_dir, filenames):
36+
"""Helper function to create test files in a temporary directory."""
37+
for filename in filenames:
38+
file_path = os.path.join(base_dir, filename)
39+
with open(file_path, "w") as f:
40+
f.write("test")
41+
42+
def test_list_files_exclude_regex():
43+
"""Test that list_files correctly excludes files matching the regex pattern."""
44+
with tempfile.TemporaryDirectory() as temp_dir:
45+
filenames = ["photo1.jpg", "photo2.png", "ignore_me.txt", "exclude_me.doc"]
46+
create_test_files(temp_dir, filenames)
47+
48+
# Test exclusion of files ending with .txt or containing "exclude"
49+
excluded_pattern = r".*\.txt|exclude.*"
50+
files = list_files(temp_dir, recursive=False, file_endings=None, exclude_pattern=excluded_pattern)
51+
52+
# Expected files: "photo1.jpg", "photo2.png" (since others match the exclude pattern)
53+
expected_files = {os.path.join(temp_dir, "photo1.jpg"), os.path.join(temp_dir, "photo2.png")}
54+
assert set(files) == expected_files
55+
56+
def test_list_files_no_exclusion():
57+
"""Test that list_files returns all files when no exclusion pattern is given."""
58+
with tempfile.TemporaryDirectory() as temp_dir:
59+
filenames = ["photo1.jpg", "photo2.png", "document.txt"]
60+
create_test_files(temp_dir, filenames)
61+
62+
files = list_files(temp_dir, recursive=False, file_endings=None, exclude_pattern=None)
63+
64+
expected_files = {os.path.join(temp_dir, f) for f in filenames}
65+
assert set(files) == expected_files
66+
67+
def test_list_files_exclude_with_endings():
68+
"""Test that list_files applies both file endings and exclude regex correctly."""
69+
with tempfile.TemporaryDirectory() as temp_dir:
70+
filenames = ["photo1.jpg", "photo2.png", "ignore.txt", "photo3.jpg"]
71+
create_test_files(temp_dir, filenames)
72+
73+
# Filter for .jpg files but exclude ones containing 'photo3'
74+
files = list_files(temp_dir, recursive=False, file_endings=[".jpg"], exclude_pattern=r".*photo3.*")
75+
76+
expected_files = {os.path.join(temp_dir, "photo1.jpg")}
77+
assert set(files) == expected_files
3578

3679
def test_list_files_non_recursive(setup_source_directory):
3780
source_dir, file_paths = setup_source_directory

0 commit comments

Comments
 (0)