diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index df25fc0..9736e7f 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -133,7 +133,7 @@ jobs: apt update apt install -t unstable -y python3-flake8 fi - - name: Workaround for https://github.com/actions/checkout/pull/762 not persisting + - name: Workaround for https://github.com/actions/checkout/issues/1169 run: git config --global --add safe.directory "$PWD" - name: Install remaining dependencies run: make venv-system-site-packages diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2429480 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing to strava-offline + +## Development + +Obtain the source code: + + $ git clone https://github.com/liskin/strava-offline + +Setup Python virtual env and install missing dependencies: + + $ make + +Make changes using your preferred editor. + +Then invoke lints, tests, …: + + $ make check + +These checks are also invoked in [CI (GitHub Actions)][ci] (against multiple +Python versions and also using different Linux distributions Python packages) +whenever a branch is pushed or a pull request is opened. You may need to +enable Actions in your fork's settings. + +Other common tasks are available in the [Makefile](Makefile): + + + + + $ make help + venv-system-site-packages: Setup ./.venv/ (--system-site-packages) + venv: Setup ./.venv/ + pipx: Install locally using pipx + pipx-site-packages: Install locally using pipx (--system-site-packages) + check: Invoke all checks (lints, tests, readme) + lint: Invoke lints + lint-flake8: + lint-mypy: + lint-isort: + test: Invoke tests + test-pytest: + test-prysk: + readme: Update usage/examples in *.md and fail if it differs from version control + dist: Build distribution artifacts (tar, wheel) + twine-upload: Release to PyPI + ipython: Invoke IPython in venv (not installed by default) + clean: Clean all gitignored files/directories + template-update: Re-render cookiecutter template into the template branch + template-merge: Re-render cookiecutter template and merge into the current branch + check-wheel: Check that the wheel we build works in a completely empty venv (i.e. check for unspecified dependencies) + help: Display this help + + +[ci]: https://github.com/liskin/strava-offline/actions + +## Style Guidelines + +* Try to follow the existing style (where it's not already enforced by a + linter). This applies to both code and git commits. + +* Familiarise yourself with [the seven rules of a great Git commit + message](https://cbea.ms/git-commit/#seven-rules). diff --git a/Makefile b/Makefile index d86c28c..bb42342 100644 --- a/Makefile +++ b/Makefile @@ -13,55 +13,63 @@ VENV_WHEEL_PYTHON = $(VENV_WHEEL)/bin/python PACKAGE := $(shell sed -ne '/^name / { y/-/_/; s/^.*=\s*"\(.*\)"/\1/p }' pyproject.toml) TEMPLATES_DIR = $(HOME)/src -TEMPLATE := $(shell realpath --relative-to=. $(TEMPLATES_DIR)/cookiecutter-python-cli) +TEMPLATE = $(eval TEMPLATE := $$(shell realpath --relative-to=. $$(TEMPLATES_DIR)/cookiecutter-python-cli))$(TEMPLATE) .PHONY: venv-system-site-packages +## Setup ./.venv/ (--system-site-packages) venv-system-site-packages: $(MAKE) VENV_USE_SYSTEM_SITE_PACKAGES=1 venv .PHONY: venv +## Setup ./.venv/ venv: $(VENV_DONE) .PHONY: pipx +## Install locally using pipx pipx: pipx install --editable . .PHONY: pipx-site-packages +## Install locally using pipx (--system-site-packages) pipx-site-packages: pipx install --system-site-packages --editable . .PHONY: check +## Invoke all checks (lints, tests, readme) check: lint test readme .PHONY: lint +## Invoke lints lint: lint-flake8 lint-mypy lint-isort LINT_SOURCES = src/ tests/ .PHONY: lint-flake8 +## lint-flake8: $(VENV_DONE) $(VENV_PYTHON) -m flake8 $(LINT_SOURCES) .PHONY: lint-mypy +## lint-mypy: $(VENV_DONE) $(VENV_PYTHON) -m mypy --show-column-numbers $(LINT_SOURCES) .PHONY: lint-isort +## lint-isort: $(VENV_DONE) $(VENV_PYTHON) -m isort --check $(LINT_SOURCES) .PHONY: test +## Invoke tests test: test-pytest test-prysk .PHONY: test-pytest +## test-pytest: $(VENV_DONE) $(VENV_PYTHON) -m pytest $(PYTEST_FLAGS) tests/ -.PHONY: readme -readme: README.md - git diff --exit-code $^ - .PHONY: test-prysk +## test-prysk: PRYSK_INTERACTIVE=$(shell [ -t 0 ] && echo --interactive) test-prysk: $(VENV_DONE) PATH="$(CURDIR)/$(VENV)/bin:$$PATH" \ @@ -70,37 +78,49 @@ test-prysk: $(VENV_DONE) $(VENV_PYTHON) -m prysk --indent=4 --shell=/bin/bash $(PRYSK_INTERACTIVE) \ $(wildcard tests/*.md tests/*/*.md tests/*/*/*.md) -.PHONY: README.md -README.md: test-prysk - tests/include.py < $@ > $@.tmp - mv -f $@.tmp $@ +.PHONY: readme +## Update usage/examples in *.md and fail if it differs from version control +readme: $(wildcard *.md) + git diff --exit-code $^ + +.PHONY: $(wildcard *.md) +$(wildcard *.md): $(VENV_DONE) test-prysk + $(VENV_PYTHON) tests/include-preproc.py --comment-start="" $@ .PHONY: dist +## Build distribution artifacts (tar, wheel) dist: $(VENV_DONE) rm -rf dist/ $(VENV_PYTHON) -m build --outdir dist .PHONY: twine-upload +## Release to PyPI twine-upload: dist $(VENV_PYTHON) -m twine upload $(wildcard dist/*) .PHONY: ipython +## Invoke IPython in venv (not installed by default) ipython: $(VENV_DONE) $(VENV_PYTHON) -m IPython .PHONY: clean +## Clean all gitignored files/directories clean: git clean -ffdX .PHONY: template-update +## Re-render cookiecutter template into the template branch template-update: $(TEMPLATE)/update.sh -t $(TEMPLATE) -p . -b template -i .cookiecutter.json .PHONY: template-merge +## Re-render cookiecutter template and merge into the current branch template-merge: template-update git merge template .PHONY: check-wheel +## Check that the wheel we build works in a completely empty venv +## (i.e. check for unspecified dependencies) check-wheel: dist $(PYTHON) -m venv --clear --without-pip $(VENV_WHEEL) cd $(VENV_WHEEL) && $(PYTHON) -m pip --isolated download pip @@ -121,7 +141,7 @@ endef # workaround for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1003252 and/or https://github.com/pypa/pip/issues/6264 ifneq ($(VENV_USE_SYSTEM_SITE_PACKAGES),) ifneq ($(shell test -f /etc/debian_version && python3 -c 'import sys; exit(not(sys.version_info < (3, 10)))' && echo x),) -$(info XXX: using SETUPTOOLS_USE_DISTUTILS=stdlib workaround) +$(warning XXX: using SETUPTOOLS_USE_DISTUTILS=stdlib workaround) $(VENV_DONE): export SETUPTOOLS_USE_DISTUTILS := stdlib endif endif @@ -130,3 +150,5 @@ $(VENV_DONE): $(MAKEFILE_LIST) pyproject.toml $(if $(VENV_USE_SYSTEM_SITE_PACKAGES),$(VENV_CREATE_SYSTEM_SITE_PACKAGES),$(VENV_CREATE)) $(VENV_PYTHON) -m pip install -e $(VENV_PIP_INSTALL) touch $@ + +include _help.mk diff --git a/README.md b/README.md index 42506e1..8162f40 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ pip install strava-offline /home/user/.config/strava_offline/config.yaml] --config-sample Show sample configuration file --help Show this message and exit. - + @@ -60,9 +60,28 @@ Sample config file can be generated using the `--config-sample` flag: $ strava-offline --config-sample - + -## Donations (♥ = €) +## Contributing + +### Code + +We welcome bug fixes, (reasonable) new features, documentation improvements, +and more. Submit these as GitHub pull requests. Use GitHub issues to report +bugs and discuss non-trivial code improvements; alternatively, get in touch +via [IRC/Matrix/Fediverse](https://work.lisk.in/contact/). + +See [CONTRIBUTING.md](CONTRIBUTING.md) for more details about the code base +(including running tests locally). + +Note that this project was born out of a desire to solve a problem I was +facing. While I'm excited to share it with the world, keep in mind that I'll +be prioritizing features and bug fixes that align with my personal use cases. +There may be times when I'm busy with other commitments and replies to +contributions might be delayed, or even occasionally missed. Progress may come +in bursts. Adjust your expectations accordingly. + +### Donations (♥ = €) If you like this tool and wish to support its development and maintenance, please consider [a small donation](https://www.paypal.me/lisknisi/10EUR) or diff --git a/_help.mk b/_help.mk new file mode 100644 index 0000000..fabb20a --- /dev/null +++ b/_help.mk @@ -0,0 +1,15 @@ +# orig src: https://github.com/liskin/dotfiles/blob/home/_help.mk + +.PHONY: help +## Display this help +help: COLUMNS=$(shell tput cols) +help: + @$(MAKE) --silent help-src \ + | perl -Mfeature=say -MText::Wrap -0777 -ne 'while(/((?:^##(?:| .*)$$(?#)\n)+)(^.*:)/gm) { my ($$t, $$h) = ($$2, $$1); $$h =~ s/^## ?//gm; $$h =~ s/\s+/ /g; $$h =~ s/^\s+|\s+$$//g; say "$$t $$h"; }' | fmt $(if $(COLUMNS),-$(COLUMNS)) -t + +.PHONY: help-src +help-src: help-src-makefiles + +.PHONY: help-src-makefiles +help-src-makefiles: + @cat $(MAKEFILE_LIST) diff --git a/tests/include-preproc.py b/tests/include-preproc.py new file mode 100755 index 0000000..4e5b290 --- /dev/null +++ b/tests/include-preproc.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +# orig src: https://github.com/liskin/dotfiles/blob/home/bin/liskin-include-preproc.py + +from __future__ import annotations # python 3.8 compat + +from dataclasses import dataclass +from pathlib import Path +import re + +import click + + +@dataclass +class Subst: + comment_start: str + comment_end: str + + def include(self, m: re.Match[str]) -> str: + filename = m[1] + + content = self.includes(Path(filename).expanduser().read_text()) + nl = "\n" if not content.endswith("\n") else "" + + cs = self.comment_start + ce = self.comment_end + return f"{cs}include {filename}{ce}\n{content}{nl}{cs}end include {filename}{ce}" + + def includes(self, s: str) -> str: + cs = re.escape(self.comment_start) + ce = re.escape(self.comment_end) + regex = f"^{cs}include (\\S+){ce}$.*?^{cs}end include \\1{ce}$" + return re.sub(regex, self.include, s, flags=re.DOTALL | re.MULTILINE) + + +@click.command(context_settings={"show_default": True}) +@click.option("--comment-start", type=str, default="## ") +@click.option("--comment-end", type=str, default="") +@click.argument("filename", type=click.Path(exists=True, allow_dash=True)) +def main(comment_start, comment_end, filename): + """ + Simple preprocessor for including files in one another. + Substitution is done in-place: the input file is modified and directives are retained so it can + serve as an input again. + """ + with click.open_file(filename, "r") as f: + input = f.read() + output = Subst(comment_start=comment_start, comment_end=comment_end).includes(input) + with click.open_file(filename, "w", atomic=True) as f: + f.write(output) + + +if __name__ == "__main__": + main() diff --git a/tests/include.py b/tests/include.py deleted file mode 100755 index acd0bd6..0000000 --- a/tests/include.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -from pathlib import Path -import re -from sys import stdin -from sys import stdout - - -def include(m: re.Match[str]) -> str: - f = Path(m[1]).read_text() - return f"\n{f}" - - -def includes(s: str) -> str: - return re.sub(r"(?s)", include, s) - - -if __name__ == "__main__": - stdout.write(includes(stdin.read())) diff --git a/tests/readme/make-help.md b/tests/readme/make-help.md new file mode 100644 index 0000000..1589d56 --- /dev/null +++ b/tests/readme/make-help.md @@ -0,0 +1,30 @@ + + + $ make help + venv-system-site-packages: Setup ./.venv/ (--system-site-packages) + venv: Setup ./.venv/ + pipx: Install locally using pipx + pipx-site-packages: Install locally using pipx (--system-site-packages) + check: Invoke all checks (lints, tests, readme) + lint: Invoke lints + lint-flake8: + lint-mypy: + lint-isort: + test: Invoke tests + test-pytest: + test-prysk: + readme: Update usage/examples in *.md and fail if it differs from version control + dist: Build distribution artifacts (tar, wheel) + twine-upload: Release to PyPI + ipython: Invoke IPython in venv (not installed by default) + clean: Clean all gitignored files/directories + template-update: Re-render cookiecutter template into the template branch + template-merge: Re-render cookiecutter template and merge into the current branch + check-wheel: Check that the wheel we build works in a completely empty venv (i.e. check for unspecified dependencies) + help: Display this help