diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cefbf7e..0e1f8f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,12 @@ repos: language: system stages: [commit] types: [python] + - id: local-fix-branch-references + name: Adjust the branch references + entry: poetry run nox -s fix-branch-references -- + language: system + stages: [commit] + types: [text] - id: local-pre-merge-checks name: Run the entire test suite entry: poetry run nox -s pre-merge -- diff --git a/noxfile.py b/noxfile.py index ec5d6f9..d12a3f5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -54,12 +54,17 @@ The pre-commit framework invokes the "pre-commit" and "pre-merge" sessions: import contextlib import glob import os +import re +import shutil +import subprocess # noqa:S404 import tempfile +from typing import Generator, IO, Tuple import nox from nox.sessions import Session +GITHUB_REPOSITORY = 'webartifex/urban-meal-delivery' PACKAGE_IMPORT_NAME = 'urban_meal_delivery' # Docs/sphinx locations. @@ -349,6 +354,94 @@ def pre_merge(session): test(session) +@nox.session(name='fix-branch-references', python=PYTHON, venv_backend='none') +def fix_branch_references(_): # noqa:WPS210 + """Replace branch references with the current branch. + + Intended to be run as a pre-commit hook. + + Many files in the project (e.g., README.md) contain links to resources + on github.com or nbviewer.jupyter.org that contain branch labels. + + This task rewrites these links such that they contain the branch reference + of the current branch. + """ + # Adjust this to add/remove glob patterns + # whose links are re-written. + paths = ['*.md', '**/*.md', '**/*.ipynb'] + + branch = ( + subprocess.check_output( # noqa:S603 + ('git', 'rev-parse', '--abbrev-ref', 'HEAD'), + ) + .decode() + .strip() + ) + + rewrites = [ + { + 'name': 'github', + 'pattern': re.compile( + fr'((((http)|(https))://github\.com/{GITHUB_REPOSITORY}/((blob)|(tree))/)([\w-]+)/)', # noqa:E501 + ), + 'replacement': fr'\2{branch}/', + }, + { + 'name': 'nbviewer', + 'pattern': re.compile( + fr'((((http)|(https))://nbviewer\.jupyter\.org/github/{GITHUB_REPOSITORY}/((blob)|(tree))/)([\w-]+)/)', # noqa:E501 + ), + 'replacement': fr'\2{branch}/', + }, + ] + + for expanded in _expand(*paths): + with _line_by_line_replace(expanded) as (old_file, new_file): + for line in old_file: + for rewrite in rewrites: + line = re.sub(rewrite['pattern'], rewrite['replacement'], line) + new_file.write(line) + + +def _expand(*patterns: str) -> Generator[str, None, None]: + """Expand glob patterns into paths. + + Args: + *patterns: the patterns to be expanded + + Yields: + expanded: a single expanded path + """ # noqa:RST213 + for pattern in patterns: + yield from glob.glob(pattern.strip()) + + +@contextlib.contextmanager +def _line_by_line_replace(path: str) -> Generator[Tuple[IO, IO], None, None]: + """Replace/change the lines in a file one by one. + + This generator function yields two file handles, one to the current file + (i.e., `old_file`) and one to its replacement (i.e., `new_file`). + + Usage: loop over the lines in `old_file` and write the files to be kept + to `new_file`. Files not written to `new_file` are removed! + + Args: + path: the file whose lines are to be replaced + + Yields: + old_file, new_file: handles to a file and its replacement + """ + file_handle, new_file_path = tempfile.mkstemp() + with os.fdopen(file_handle, 'w') as new_file: + with open(path) as old_file: + yield old_file, new_file + + shutil.copymode(path, new_file_path) + os.remove(path) + shutil.move(new_file_path, path) + + @nox.session(name='init-project', python=PYTHON, venv_backend='none') def init_project(session): """Install the pre-commit hooks.""" @@ -369,17 +462,15 @@ def clean_pwd(session): # noqa:WPS210,WPS231 with open('.gitignore') as file_handle: paths = file_handle.readlines() - for path in paths: - path = path.strip() + for path in _expand(*paths): if path.startswith('#'): continue - for expanded in glob.glob(path): - for excluded in exclude: - if expanded.startswith(excluded): - break - else: - session.run('rm', '-rf', expanded) + for excluded in exclude: + if path.startswith(excluded): + break + else: + session.run('rm', '-rf', path) def _begin(session): diff --git a/setup.cfg b/setup.cfg index 1bbb117..5bbd00d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -88,7 +88,7 @@ extend-ignore = B950, # Comply with black's style. # Source: https://github.com/psf/black/blob/master/docs/compatible_configs.md#flake8 - E203, W503, + E203, W503, WPS348, # f-strings are ok. WPS305, # Classes should not have to specify a base class. @@ -125,6 +125,8 @@ per-file-ignores = WPS213, # No overuse of string constants (e.g., '--version'). WPS226, + # The noxfile is rather long => allow many noqa's. + WPS402, src/urban_meal_delivery/configuration.py: # Allow upper case class variables within classes. WPS115,