Browse Source

Initial working commit with some tests

master
IamTheFij 4 months ago
parent
commit
2b7acbc3b1
  1. 16
      .drone.star
  2. 18
      Makefile
  3. 46
      README.md
  4. 52
      docs/source/conf.py
  5. 20
      docs/source/index.rst
  6. 155
      padio.py
  7. 3
      requirements-dev.txt
  8. 8
      setup.py
  9. 90
      tests/test_paddy.py
  10. 16
      tox.ini

16
.drone.star

@ -99,16 +99,16 @@ def notify_step():
# Builds a notify pipeline that will notify when a dependency fails
def notify(depends_on=None):
if depends_on is None:
if not depends_on:
depends_on = ["tests"]
return list(dict(
kind="pipeline",
name="notify",
depends_on=depends_on,
trigger=dict(status=["failure"]),
steps=[notify_step()]
))
return [{
"kind": "pipeline",
"name": "notify",
"depends_on": depends_on,
"trigger": {"status": ["failure"]},
"steps": [notify_step()]
}]
# Push package to pypi

18
Makefile

@ -1,5 +1,5 @@
OPEN_CMD := $(shell type xdg-open &> /dev/null && echo 'xdg-open' || echo 'open')
NAME := paddy
NAME := padio
ENV := env
.PHONY: default
@ -98,19 +98,3 @@ htmlcov/index.html: .coverage
.PHONY: open-coverage
open-coverage: htmlcov/index.html
$(OPEN_CMD) htmlcov/index.html
# Cleans out docs
.PHONY: docs-clean
docs-clean:
rm -fr docs/build/* docs/source/code/*
# Builds docs
docs/build/html/index.html:
$(ENV)/bin/tox -e docs
# Shorthand for building docs
.PHONY: docs
docs: docs/build/html/index.html
.PHONY: clean-all
clean-all: clean dist-clean docs-clean

46
README.md

@ -1,7 +1,49 @@
# paddy
# padio
Zero pad numeric filenames
Turn a bunch of files like this:
file1.txt
file10.txt
file5.txt
and want them to be sorted like this:
file01.txt
file05.txt
file10.txt
you can run:
padio *.txt
## Installation
pip install padio
## Usage
usage: padio [-h] [-l LENGTH] [-f] [-v] [-d] [-i REGEX] [--ignore-files IGNOREFILE [IGNOREFILE ...]] file [file ...]
Pads numbers in file names so they consistently align and sort
positional arguments:
file Files to be renamed
optional arguments:
-h, --help show this help message and exit
-l LENGTH, --length LENGTH
Length of numbers after padding (default: auto)
-f, --force Force rename, even if file at destination exists
-v, --verbose Print all actions
-d, --dry-run Print actions only without modifying any file. Implies --verbose
-i REGEX, --ignore REGEX
Regular expression used to ignore files matching the name
--ignore-files IGNOREFILE [IGNOREFILE ...]
Files to ignore for renaming. Must add -- before positional arguments
## Original repo
Originally hosted at https://git.iamthefij.com/iamthefij/paddy.git
Originally hosted at https://git.iamthefij.com/iamthefij/padio.git

52
docs/source/conf.py

@ -1,52 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'paddy'
copyright = '2021, iamthefij'
author = 'iamthefij'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

20
docs/source/index.rst

@ -1,20 +0,0 @@
.. paddy documentation master file, created by
sphinx-quickstart on Fri Feb 5 15:06:05 2021.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to paddy's documentation!
========================================
.. toctree::
:maxdepth: 2
:caption: Contents:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

155
padio.py

@ -0,0 +1,155 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import re
from pathlib import Path
from typing import Generator
from typing import List
from typing import Optional
from typing import Tuple
num_matcher = re.compile(r"([0-9]+)")
def extract_numbers(s: str) -> str:
number_match = num_matcher.search(s)
if number_match:
return number_match.group(1)
return ""
def calc_pad_length(files: List[Path]) -> int:
max_len = 0
for f in files:
numbers = extract_numbers(f.name)
max_len = max(len(numbers), max_len)
return max_len
def parse_args(sys_args: Optional[List[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Pads numbers in file names so they consistently align and sort"
)
parser.add_argument("files", nargs="+", metavar="file", help="Files to be renamed")
parser.add_argument(
"-l",
"--length",
type=int,
help="Length of numbers after padding (default: auto)",
)
parser.add_argument(
"-f",
"--force",
action="store_true",
help="Force rename, even if file at destination exists",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Print all actions"
)
parser.add_argument(
"-d",
"--dry-run",
action="store_true",
help="Print actions only without modifying any file. Implies --verbose",
)
parser.add_argument(
"-i",
"--ignore",
metavar="REGEX",
help="Regular expression used to ignore files matching the name",
)
parser.add_argument(
"--ignore-files",
nargs="+",
metavar="IGNOREFILE",
help="Files to ignore for renaming. Must add -- before positional arguments",
)
args = parser.parse_args(sys_args)
# Dry run implies verbose
if args.dry_run:
args.verbose = True
return args
def get_files(
filenames: List[str],
ignore_filenames: List[str],
ignore_pattern: str,
) -> List[Path]:
# Compile ignore pattern, if provided
ignore_matcher: Optional[re.Pattern] = None
if ignore_pattern:
ignore_matcher = re.compile(ignore_pattern)
p = Path(".")
files: List[Path] = []
for f in filenames:
if ignore_filenames and f in ignore_filenames:
continue
if ignore_matcher and ignore_matcher.match(f):
continue
files.append(p / f)
return files
def pad_files(
files: List[Path],
pad_len: int,
verbose=False,
) -> Generator[Tuple[Path, Path], None, None]:
for f in files:
numbers = extract_numbers(f.name)
if len(numbers) == pad_len:
if verbose:
print(f"{f.name} is already padded.")
continue
# Pad number and get destination path
new_numbers = numbers.zfill(pad_len)
new_name = num_matcher.sub(new_numbers, f.name, count=1)
new_file = f.parent / new_name
if f == new_file:
if verbose:
print(f"{f.name} already matches destination.")
continue
yield f, new_file
def main(sys_args: Optional[List[str]] = None) -> int:
args = parse_args(sys_args)
# Build list of files to act on
files = get_files(args.files, args.ignore_files, args.ignore)
pad_len = args.length
if pad_len is None:
pad_len = calc_pad_length(files)
if args.verbose:
print(f"Padding to {pad_len}")
status = 0
for f, new_file in pad_files(files, pad_len, args.verbose):
# Possibly rename unless exists or forced
if not new_file.exists() or args.force:
if args.verbose:
print(f"Rename {f.name} to {new_file.name}")
if not args.dry_run:
f.rename(new_file)
else:
print(
f"Could not rename {f.name} to {new_file.name}. Destination file exists."
)
status = 1
return status
if __name__ == "__main__":
exit(main())

3
requirements-dev.txt

@ -0,0 +1,3 @@
-e .
pytest
coverage

8
setup.py

@ -11,13 +11,13 @@ with open(path.join(here, "README.md"), encoding="utf-8") as f:
long_description = f.read()
setup(
name="paddy",
name="padio",
version="0.0.0",
description="Zero pad numeric filenames",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://git.iamthefij.com/iamthefij/paddy.git",
download_url=("https://git.iamthefij.com/iamthefij/paddy.git/archive/master.tar.gz"),
url="https://git.iamthefij.com/iamthefij/padio.git",
download_url=("https://git.iamthefij.com/iamthefij/padio.git/archive/master.tar.gz"),
author="iamthefij",
author_email="",
classifiers=[
@ -39,7 +39,7 @@ setup(
install_requires=[],
entry_points={
"console_scripts": [
"paddy=paddy.main:main",
"padio=padio:main",
],
},
)

90
tests/test_paddy.py

@ -0,0 +1,90 @@
from pathlib import Path
import pytest
import padio
@pytest.mark.parametrize(
"value,expected",
(
("file123", "123"),
("123file", "123"),
("123file456", "123"),
("file", ""),
),
)
def test_extract_numbers(value, expected):
assert padio.extract_numbers(value) == expected
@pytest.mark.parametrize(
"file_paths,expected",
(
([Path("/path/to/file.a123.txt"), Path("/path/to/file.b123.txt")], 3),
([Path("/path/to/file.a023.txt"), Path("/path/to/file.b023.txt")], 3),
([Path("/path/to/file.a123.txt"), Path("/path/to/file.b12.txt")], 3),
),
)
def test_calc_pad_length(file_paths, expected):
assert padio.calc_pad_length(file_paths) == expected
@pytest.mark.parametrize(
"files,ignore_files,ignore,expected",
(
(
[
"file.a123.txt",
"file.b123.txt",
],
[],
"",
[
Path("./file.a123.txt"),
Path("./file.b123.txt"),
],
),
(
[
"file.a123.txt",
"file.b123.txt",
],
[
"file.a123.txt",
],
"",
[
Path("./file.b123.txt"),
],
),
(
[
"file.a123.txt",
"file.b123.txt",
"ignore.txt"
],
[
"file.a123.txt",
],
"ignore.*",
[
Path("./file.b123.txt"),
],
),
),
)
def test_get_files(files, ignore_files, ignore, expected):
assert padio.get_files(files, ignore_files, ignore) == expected
@pytest.mark.parametrize(
"files,pad_len,expected",
(
([Path("./file.a12")], 1, []),
([Path("./file.a12")], 2, []),
([Path("./file.a12")], 3, [Path("./file.a012")]),
),
)
def test_pad_files(files, pad_len, expected):
assert list(padio.pad_files(files, pad_len)) == list(zip(files, expected))

16
tox.ini

@ -1,13 +1,13 @@
[tox]
envlist = py3,pypy3
envlist = py3,py36,py37,py38,pypy3
[testenv]
deps =
-rrequirements-dev.txt
commands =
coverage erase
coverage run --source=paddy/ -m pytest --capture=no -vv {posargs:tests}
coverage report -m --fail-under 70
coverage run --source=padio -m pytest --capture=no -vv {posargs:tests}
coverage report -m --fail-under 50
pre-commit run --all-files
[testenv:pre-commit]
@ -15,13 +15,3 @@ deps =
pre-commit
commands =
pre-commit {posargs}
[testenv:docs]
deps =
{[base]deps}
sphinx
sphinx_rtd_theme
changedir = docs
commands =
sphinx-apidoc -f -e -o source/code ../paddy
sphinx-build -b html -d build/doctrees source/ build/html
Loading…
Cancel
Save