diff --git a/.flake8 b/.flake8 index c1daeba..57a6c02 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,5 @@ [flake8] max-line-length = 132 -exclude = .git,__pycache__,.eggs/,doc/,docs/,build/,dist/,archive/ \ No newline at end of file +exclude = .git,__pycache__,.eggs/,doc/,docs/,build/,dist/,archive/ +per-file-ignores = + __init__.py:F401 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..702f5b3 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: [scivision] +ko_fi: scivision diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5a4b342 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: ci + +on: + push: + paths: + - "**.py" + - ".github/workflows/ci.yml" + pull_request: + paths: + - "**.py" + - ".github/workflows/ci.yml" + +jobs: + + linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + with: + python-version: '3.x' + - run: pip install .[tests,lint] + - run: flake8 + - run: mypy . + - run: pytest + working-directory: tests + + integration: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, macos-latest] + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + with: + python-version: '3.x' + - run: pip install .[tests] + - run: pytest + working-directory: tests diff --git a/README.md b/README.md index 0fecf34..7243c6d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,12 @@ -[![DOI](https://zenodo.org/badge/186922933.svg)](https://zenodo.org/badge/latestdoi/186922933) - -[![Build Status](https://travis-ci.com/scivision/meldutils.svg?branch=master)](https://travis-ci.com/scivision/meldutils) -[![Coverage Status](https://coveralls.io/repos/github/scivision/meldutils/badge.svg?branch=master)](https://coveralls.io/github/scivision/meldutils?branch=master) -[![Build status](https://ci.appveyor.com/api/projects/status/l2qshn68va0by813?svg=true)](https://ci.appveyor.com/project/scivision/meldutils) - # Meld Utilities -Python scripts using [Meld](https://meldmerge.org) to accomplish tasks useful for managing large numbers of repos / projects, particularly for templates. -It works on any OS Meld works on (Linux, MacOS, Windows). +[![DOI](https://zenodo.org/badge/186922933.svg)](https://zenodo.org/badge/latestdoi/186922933) +[![Actions Status](https://github.com/scivision/meldutils/workflows/ci/badge.svg)](https://github.com/scivision/meldutils/actions) +Python scripts using +[Meld](https://meldmerge.org) +to accomplish tasks useful for managing large numbers of repos / projects, particularly for templates. +It works on any OS Meld works on (Linux, MacOS, Windows). ## Scripts @@ -18,7 +16,6 @@ meld_all project/myfile.f90 ~/code That terminal command invokes `meld` between `project/myfile.f90` and every other file with the same name found recursively under `~/code`. - ### Usage Particularly on Windows, you may get Meld brought up and you don't see any difference. diff --git a/.appveyor.yml b/archive/.appveyor.yml similarity index 100% rename from .appveyor.yml rename to archive/.appveyor.yml diff --git a/.travis.yml b/archive/.travis.yml similarity index 100% rename from .travis.yml rename to archive/.travis.yml diff --git a/meld_all.py b/meld_all.py index eaaad89..362bcd2 100644 --- a/meld_all.py +++ b/meld_all.py @@ -10,94 +10,38 @@ """ from pathlib import Path from argparse import ArgumentParser -from typing import Iterator -import filecmp +import meldutils as mu import logging -import subprocess -import shutil -try: - import ghlinguist as ghl -except (ImportError, FileNotFoundError): - ghl = None - - -def files_to_meld(root: Path, ref: Path, - language: str = None, - strict: bool = False) -> Iterator[Path]: - - si = 1 if strict else 2 - - ref = Path(ref).expanduser() - if not ref.is_file(): - raise FileNotFoundError(f'specify a reference file, not a directory {ref}') - - root = Path(root).expanduser() - if not root.is_dir(): - raise NotADirectoryError(root) - - for new in root.rglob(ref.name): - if new.samefile(ref): - continue - - if filecmp.cmp(new, ref, shallow=False): # type: ignore # mypy .pyi needs updating - logging.info(f'SAME: {new.parent}') - continue - - if language and ghl is not None: - langlist = ghl.linguist(new.parent) - if langlist is None: - logging.info(f'SKIP: {new.parent}') - continue - - thislangs = [l[0] for l in langlist[:si]] - if language not in thislangs: - logging.info(f'SKIP: {new.parent} {thislangs}') - continue - - yield new - - -def meld_files(ref: Path, new: Path, rexe: str): - """ - run file comparison program (often meld) on file pair - """ - - exe = shutil.which(rexe) - if not exe: - logging.critical('File comparison program not found. Try -n option to just see which files differ.') - raise FileNotFoundError(rexe) - # Not using check_call due to spurious errors - new = Path(new).expanduser() - ref = Path(ref).expanduser() - subprocess.run([exe, str(ref), str(new)]) def main(): p = ArgumentParser() - p.add_argument('ref', help='filename to compare against') - p.add_argument('root', help='top-level directory to search under') - p.add_argument('-l', '--language', help='language to template') - p.add_argument('-exe', help='program to compare with', default='meld') - p.add_argument('-s', '--strict', help='compare only with first language match', action='store_true') - p.add_argument('-n', '--dryrun', help='just report files that are different', action='store_true') + p.add_argument("ref", help="filename to compare against") + p.add_argument("root", help="top-level directory to search under") + p.add_argument("-l", "--language", help="language to template") + p.add_argument("-exe", help="program to compare with", default="meld") + p.add_argument("-s", "--strict", help="compare only with first language match", action="store_true") + p.add_argument("-n", "--dryrun", help="just report files that are different", action="store_true") p = p.parse_args() if p.dryrun: level = logging.INFO else: level = logging.WARNING - logging.basicConfig(format='%(message)s', level=level) + logging.basicConfig(format="%(message)s", level=level) root = Path(p.root).expanduser() + if not root.is_dir(): + raise SystemExit(f"{root} is not a directory") - files = files_to_meld(root, p.ref, p.language, strict=p.strict) + files = mu.files_to_meld(root, p.ref, p.language, strict=p.strict) for file in files: if p.dryrun: - print(f'{file} != {p.ref}') + print(f"{file} != {p.ref}") else: - meld_files(p.ref, file, p.exe) + mu.meld_files(p.ref, file, p.exe) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/meldutils/__init__.py b/meldutils/__init__.py new file mode 100644 index 0000000..8b33622 --- /dev/null +++ b/meldutils/__init__.py @@ -0,0 +1 @@ +from .base import files_to_meld, meld_files diff --git a/meldutils/base.py b/meldutils/base.py new file mode 100644 index 0000000..d1fc7be --- /dev/null +++ b/meldutils/base.py @@ -0,0 +1,59 @@ +import typing +from pathlib import Path +import shutil +import subprocess +import logging +import filecmp + +try: + import ghlinguist as ghl +except (ImportError, FileNotFoundError): + ghl = None + + +def files_to_meld(root: Path, ref: Path, language: str = None, strict: bool = False) -> typing.Iterator[Path]: + + si = 1 if strict else 2 + + ref = Path(ref).expanduser() + if not ref.is_file(): + raise FileNotFoundError(f"specify a reference file, not a directory {ref}") + + root = Path(root).expanduser() + if not root.is_dir(): + raise NotADirectoryError(f"{root} is not a directory") + + for new in root.rglob(ref.name): + if new.samefile(ref): + continue + + if filecmp.cmp(new, ref, shallow=False): # type: ignore # mypy .pyi needs updating + logging.info(f"SAME: {new.parent}") + continue + + if language and ghl is not None: + langlist = ghl.linguist(new.parent) + if langlist is None: + logging.info(f"SKIP: {new.parent}") + continue + + thislangs = [l[0] for l in langlist[:si]] + if language not in thislangs: + logging.info(f"SKIP: {new.parent} {thislangs}") + continue + + yield new + + +def meld_files(ref: Path, new: Path, rexe: str): + """ + run file comparison program (often meld) on file pair + """ + + exe = shutil.which(rexe) + if not exe: + raise FileNotFoundError(f"{rexe} not found. Try -n option to just see which files differ.") + # Not using check_call due to spurious errors + new = Path(new).expanduser() + ref = Path(ref).expanduser() + subprocess.run([exe, str(ref), str(new)]) diff --git a/pyproject.toml b/pyproject.toml index c953815..2f2c683 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,5 @@ [build-system] -requires = ["setuptools", "wheel"] \ No newline at end of file +requires = ["setuptools", "wheel"] + +[tool.black] +line-length = 132 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b175c60 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -ra -v diff --git a/setup.cfg b/setup.cfg index a08ee64..8d53d25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = meldutils -version = 1.0.2 +version = 1.1.0 author = Michael Hirsch, Ph.D. author_email = scivision@users.noreply.github.com description = Python utilities for Meld, good for managing templates across lots of repos / projects @@ -18,6 +18,8 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Topic :: Software Development :: Version Control Topic :: Utilities license_files = LICENSE.txt @@ -34,9 +36,7 @@ install_requires = [options.extras_require] tests = pytest -cov = - pytest-cov - coveralls +lint = flake8 mypy linguist = diff --git a/test_meld.py b/tests/test_meld.py similarity index 71% rename from test_meld.py rename to tests/test_meld.py index 61268ed..ec192b1 100644 --- a/test_meld.py +++ b/tests/test_meld.py @@ -3,13 +3,13 @@ from pathlib import Path import os -import meld_all as mu +import meldutils as mu @pytest.fixture def gen_file(tmp_path): - f1 = tmp_path/'a/hi.txt' - f2 = tmp_path/'b/hi.txt' + f1 = tmp_path / "a/hi.txt" + f2 = tmp_path / "b/hi.txt" make_file(f1) make_file(f2) @@ -19,7 +19,7 @@ def gen_file(tmp_path): def make_file(path: Path): path.parent.mkdir(exist_ok=True, parents=True) - path.write_text('hello') + path.write_text("hello") def test_find(gen_file): @@ -30,8 +30,8 @@ def test_find(gen_file): assert len(files) == 0 # add a whitespace to one file to make the file slightly different than the other file - with f2.open('a') as f: - f.write(' ') + with f2.open("a") as f: + f.write(" ") files = list(mu.files_to_meld(f1.parents[1], f1)) @@ -40,15 +40,15 @@ def test_find(gen_file): def test_diff(gen_file): f1, f2 = gen_file - f2.write_text('hi') + f2.write_text("hi") - if os.name == 'nt': - diff = 'FC' + if os.name == "nt": + diff = "FC" else: - diff = 'diff' + diff = "diff" mu.meld_files(f1.parents[1], f1, diff) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main([__file__])