Initial working commit with some tests
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
843b2d833e
commit
2b7acbc3b1
16
.drone.star
16
.drone.star
@ -99,16 +99,16 @@ def notify_step():
|
|||||||
|
|
||||||
# Builds a notify pipeline that will notify when a dependency fails
|
# Builds a notify pipeline that will notify when a dependency fails
|
||||||
def notify(depends_on=None):
|
def notify(depends_on=None):
|
||||||
if depends_on is None:
|
if not depends_on:
|
||||||
depends_on = ["tests"]
|
depends_on = ["tests"]
|
||||||
|
|
||||||
return list(dict(
|
return [{
|
||||||
kind="pipeline",
|
"kind": "pipeline",
|
||||||
name="notify",
|
"name": "notify",
|
||||||
depends_on=depends_on,
|
"depends_on": depends_on,
|
||||||
trigger=dict(status=["failure"]),
|
"trigger": {"status": ["failure"]},
|
||||||
steps=[notify_step()]
|
"steps": [notify_step()]
|
||||||
))
|
}]
|
||||||
|
|
||||||
|
|
||||||
# Push package to pypi
|
# Push package to pypi
|
||||||
|
18
Makefile
18
Makefile
@ -1,5 +1,5 @@
|
|||||||
OPEN_CMD := $(shell type xdg-open &> /dev/null && echo 'xdg-open' || echo 'open')
|
OPEN_CMD := $(shell type xdg-open &> /dev/null && echo 'xdg-open' || echo 'open')
|
||||||
NAME := paddy
|
NAME := padio
|
||||||
ENV := env
|
ENV := env
|
||||||
|
|
||||||
.PHONY: default
|
.PHONY: default
|
||||||
@ -98,19 +98,3 @@ htmlcov/index.html: .coverage
|
|||||||
.PHONY: open-coverage
|
.PHONY: open-coverage
|
||||||
open-coverage: htmlcov/index.html
|
open-coverage: htmlcov/index.html
|
||||||
$(OPEN_CMD) 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
46
README.md
@ -1,7 +1,49 @@
|
|||||||
# paddy
|
# padio
|
||||||
|
|
||||||
Zero pad numeric filenames
|
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
|
## Original repo
|
||||||
|
|
||||||
Originally hosted at https://git.iamthefij.com/iamthefij/paddy.git
|
Originally hosted at https://git.iamthefij.com/iamthefij/padio.git
|
||||||
|
@ -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']
|
|
@ -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
Executable file
155
padio.py
Executable file
@ -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
Normal file
3
requirements-dev.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-e .
|
||||||
|
pytest
|
||||||
|
coverage
|
8
setup.py
8
setup.py
@ -11,13 +11,13 @@ with open(path.join(here, "README.md"), encoding="utf-8") as f:
|
|||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="paddy",
|
name="padio",
|
||||||
version="0.0.0",
|
version="0.0.0",
|
||||||
description="Zero pad numeric filenames",
|
description="Zero pad numeric filenames",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
url="https://git.iamthefij.com/iamthefij/paddy.git",
|
url="https://git.iamthefij.com/iamthefij/padio.git",
|
||||||
download_url=("https://git.iamthefij.com/iamthefij/paddy.git/archive/master.tar.gz"),
|
download_url=("https://git.iamthefij.com/iamthefij/padio.git/archive/master.tar.gz"),
|
||||||
author="iamthefij",
|
author="iamthefij",
|
||||||
author_email="",
|
author_email="",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
@ -39,7 +39,7 @@ setup(
|
|||||||
install_requires=[],
|
install_requires=[],
|
||||||
entry_points={
|
entry_points={
|
||||||
"console_scripts": [
|
"console_scripts": [
|
||||||
"paddy=paddy.main:main",
|
"padio=padio:main",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
90
tests/test_paddy.py
Normal file
90
tests/test_paddy.py
Normal file
@ -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
16
tox.ini
@ -1,13 +1,13 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py3,pypy3
|
envlist = py3,py36,py37,py38,pypy3
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
-rrequirements-dev.txt
|
-rrequirements-dev.txt
|
||||||
commands =
|
commands =
|
||||||
coverage erase
|
coverage erase
|
||||||
coverage run --source=paddy/ -m pytest --capture=no -vv {posargs:tests}
|
coverage run --source=padio -m pytest --capture=no -vv {posargs:tests}
|
||||||
coverage report -m --fail-under 70
|
coverage report -m --fail-under 50
|
||||||
pre-commit run --all-files
|
pre-commit run --all-files
|
||||||
|
|
||||||
[testenv:pre-commit]
|
[testenv:pre-commit]
|
||||||
@ -15,13 +15,3 @@ deps =
|
|||||||
pre-commit
|
pre-commit
|
||||||
commands =
|
commands =
|
||||||
pre-commit {posargs}
|
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…
Reference in New Issue
Block a user