From 92f702678f3c57d4d9b027067e7eeb498e0ac028 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 12 Jul 2023 16:01:34 +0200 Subject: [PATCH 1/5] Added pre-commit config (+flake8) --- .flake8 | 2 ++ .pre-commit-config.yaml | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..2bcd70e3 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 88 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..b3ce741a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +default_language_version: + python: python3 + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-toml + - repo: https://github.com/pycqa/isort + rev: '5.12.0' + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: '23.7.0' + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + rev: '6.0.0' + hooks: + - id: flake8 From 12ab21d4a8ed437dd91ab5410e9b5df58b0e5b80 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 12 Jul 2023 16:06:07 +0200 Subject: [PATCH 2/5] black, isort, flake8 --- .flake8 | 1 + doc/source/conf.py | 2 +- doc/source/versions.rst | 2 +- zest/releaser/baserelease.py | 5 ++- zest/releaser/longtest.py | 3 +- zest/releaser/prerelease.py | 5 ++- zest/releaser/pypi.py | 29 ++++++++---- zest/releaser/release.py | 70 +++++++++++++++++------------ zest/releaser/tests/baserelease.txt | 2 +- zest/releaser/tests/cmd_error.py | 1 + zest/releaser/tests/functional.py | 3 +- zest/releaser/tests/test_setup.py | 1 - zest/releaser/utils.py | 66 +++++++++++++-------------- zest/releaser/vcs.py | 2 +- 14 files changed, 110 insertions(+), 82 deletions(-) diff --git a/.flake8 b/.flake8 index 2bcd70e3..cadcae03 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,3 @@ [flake8] max-line-length = 88 +ignore = E203, E501, W503 diff --git a/doc/source/conf.py b/doc/source/conf.py index afbc92a0..cb327cee 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -181,7 +181,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ("index", "%s.tex" % project, u"%s Documentation" % project, author, "manual"), + ("index", "%s.tex" % project, "%s Documentation" % project, author, "manual"), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/doc/source/versions.rst b/doc/source/versions.rst index 9096b96d..9acb2f34 100644 --- a/doc/source/versions.rst +++ b/doc/source/versions.rst @@ -49,7 +49,7 @@ A version number can come from various different locations: [zest.releaser] python-file-with-version = mypackage/__init__.py - + Alternatively, in ``pyproject.toml``, you can use the following:: [tool.zest-releaser] diff --git a/zest/releaser/baserelease.py b/zest/releaser/baserelease.py index bf888af1..c7c442a3 100644 --- a/zest/releaser/baserelease.py +++ b/zest/releaser/baserelease.py @@ -77,7 +77,9 @@ def __init__(self, vcs=None): "zest.releaser.tests", "pypirc_old.txt" ) self.pypiconfig = pypi.PypiConfig(pypirc_old) - self.zest_releaser_config = pypi.ZestReleaserConfig(pypirc_config_filename=pypirc_old) + self.zest_releaser_config = pypi.ZestReleaserConfig( + pypirc_config_filename=pypirc_old + ) else: self.pypiconfig = pypi.PypiConfig() self.zest_releaser_config = pypi.ZestReleaserConfig() @@ -86,7 +88,6 @@ def __init__(self, vcs=None): @property def history_format(self): - default = "rst" config_value = self.zest_releaser_config.history_format() history_file = self.data.get("history_file") or "" return utils.history_format(config_value, history_file) diff --git a/zest/releaser/longtest.py b/zest/releaser/longtest.py index 6de54a59..f660006b 100644 --- a/zest/releaser/longtest.py +++ b/zest/releaser/longtest.py @@ -80,7 +80,8 @@ def main(): action="store_true", dest="headless", default=False, - help="Do not open a browser window with the HTML result") + help="Do not open a browser window with the HTML result", + ) options = utils.parse_options(parser) utils.configure_logging() code = show_longdesc(headless=options.headless) diff --git a/zest/releaser/prerelease.py b/zest/releaser/prerelease.py index 8829cc1f..cdb89d48 100644 --- a/zest/releaser/prerelease.py +++ b/zest/releaser/prerelease.py @@ -15,6 +15,7 @@ # non-Python projects. from pep440 import is_canonical except ImportError: + def is_canonical(version): logger.debug("Using dummy is_canonical that always returns True.") return True @@ -109,7 +110,9 @@ def _grab_version(self, initial=False): while new_version is None: new_version = utils.ask_version("Enter version", default=suggestion) if not is_canonical(new_version): - logger.warning(f"'{new_version}' is not a canonical Python package version.") + logger.warning( + f"'{new_version}' is not a canonical Python package version." + ) question = "Do you want to use this version anyway?" if not utils.ask(question): # Set to None: we will ask to enter a new version. diff --git a/zest/releaser/pypi.py b/zest/releaser/pypi.py index 77e31107..154542dc 100644 --- a/zest/releaser/pypi.py +++ b/zest/releaser/pypi.py @@ -1,11 +1,13 @@ -from configparser import ConfigParser, NoOptionError, NoSectionError +from .utils import extract_zestreleaser_configparser +from configparser import ConfigParser +from configparser import NoOptionError +from configparser import NoSectionError import logging import os import pkg_resources import sys -from .utils import string_to_bool, extract_zestreleaser_configparser try: # Python 3.11+ @@ -50,8 +52,7 @@ def _get_boolean(self, section, key, default=False): return result def _get_text(self, section, key, default=None, raw=False): - """Get a text from the config. - """ + """Get a text from the config.""" result = default if self.config is not None: try: @@ -142,7 +143,7 @@ def zest_releaser_config(self): class PypiConfig(BaseConfig): """Wrapper around the pypi config file. - + Contains functions which return information about the pypi configuration. """ @@ -166,7 +167,7 @@ def reload(self): settings, and tell release to retry the command. """ self._read_configfile() - + def zest_releaser_config(self): return extract_zestreleaser_configparser(self.config, self.config_filename) @@ -262,7 +263,9 @@ def zest_releaser_config(self): try: result = self.config["tool"]["zest-releaser"] except KeyError: - logger.debug(f"No [tool.zest-releaser] section found in the {self.config_filename}") + logger.debug( + f"No [tool.zest-releaser] section found in the {self.config_filename}" + ) return None return result @@ -283,13 +286,21 @@ def load_configs(self, pypirc_config_filename=DIST_CONFIG_FILE): combined_config.update(zest_config) # store which config file contained entrypoint hooks - if any([x for x in zest_config.keys() if x.lower().startswith(("prereleaser.", "releaser.", "postreleaser."))]): + if any( + [ + x + for x in zest_config.keys() + if x.lower().startswith( + ("prereleaser.", "releaser.", "postreleaser.") + ) + ] + ): self.hooks_filename = config.config_filename self.config = combined_config def __init__(self, pypirc_config_filename=DIST_CONFIG_FILE): self.load_configs(pypirc_config_filename=pypirc_config_filename) - + def want_release(self): """Does the user normally want to release this package. diff --git a/zest/releaser/release.py b/zest/releaser/release.py index 374da2c4..1d1f24ba 100644 --- a/zest/releaser/release.py +++ b/zest/releaser/release.py @@ -1,10 +1,12 @@ # GPL, (c) Reinout van Rees +from build import ProjectBuilder from colorama import Fore +from subprocess import CalledProcessError +from subprocess import check_output +from subprocess import STDOUT from urllib import request from urllib.error import HTTPError -from build import ProjectBuilder -from subprocess import check_output, CalledProcessError, STDOUT import logging import os @@ -59,30 +61,35 @@ def package_in_pypi(package): logger.debug("Package not found on pypi: %s", e) return False + def _project_builder_runner(cmd, cwd=None, extra_environ=None): - """Run the build command and format warnings and errors. - - It runs the build command in a subprocess. Warnings and errors are formatted - in red so that they will work correctly with utils.show_interesting_lines(). We - mimic the setuptools/wheels output that way. - """ - env = os.environ.copy() - if extra_environ: - env.update(extra_environ) + """Run the build command and format warnings and errors. - try: - result = check_output(cmd, cwd=cwd, env=env, stderr=STDOUT) - except CalledProcessError as e: - raise SystemExit(f"Build failed with the following error:\n{e.output.decode()}\nExiting") from e - result_split = result.split(b"\n") - formatted_result = [] - for line in result_split: - line = line.decode() - if line.lower().startswith(("warning", "error")): - line = Fore.RED + line + Fore.RESET # reset so that not all the lines after a warning are red - formatted_result.append(line) - formatted_result_joined = "\n".join(formatted_result) - utils.show_interesting_lines(formatted_result_joined) + It runs the build command in a subprocess. Warnings and errors are formatted + in red so that they will work correctly with utils.show_interesting_lines(). We + mimic the setuptools/wheels output that way. + """ + env = os.environ.copy() + if extra_environ: + env.update(extra_environ) + + try: + result = check_output(cmd, cwd=cwd, env=env, stderr=STDOUT) + except CalledProcessError as e: + raise SystemExit( + f"Build failed with the following error:\n{e.output.decode()}\nExiting" + ) from e + result_split = result.split(b"\n") + formatted_result = [] + for line in result_split: + line = line.decode() + if line.lower().startswith(("warning", "error")): + line = ( + Fore.RED + line + Fore.RESET + ) # reset so that not all the lines after a warning are red + formatted_result.append(line) + formatted_result_joined = "\n".join(formatted_result) + utils.show_interesting_lines(formatted_result_joined) class Releaser(baserelease.Basereleaser): @@ -102,7 +109,9 @@ def prepare(self): self._grab_version() tag = self.zest_releaser_config.tag_format(self.data["version"]) self.data["tag"] = tag - self.data["tag-message"] = self.zest_releaser_config.tag_message(self.data["version"]) + self.data["tag-message"] = self.zest_releaser_config.tag_message( + self.data["version"] + ) self.data["tag-signing"] = self.zest_releaser_config.tag_signing() self.data["tag_already_exists"] = self.vcs.tag_exists(tag) @@ -158,14 +167,14 @@ def _upload_distributions(self, package): "Making a source distribution of a fresh tag checkout (in %s).", self.data["tagworkingdir"], ) - builder = ProjectBuilder(srcdir='.', runner=_project_builder_runner) - builder.build('sdist', './dist/') + builder = ProjectBuilder(srcdir=".", runner=_project_builder_runner) + builder.build("sdist", "./dist/") if self.zest_releaser_config.create_wheel(): logger.info( "Making a wheel of a fresh tag checkout (in %s).", self.data["tagworkingdir"], ) - builder.build('wheel', './dist/') + builder.build("wheel", "./dist/") if not self.pypiconfig.is_pypi_configured(): logger.error( "You must have a properly configured %s file in " @@ -344,7 +353,10 @@ def _release(self): # Run extra entry point self._run_hooks("after_checkout") - if any(filename in os.listdir(self.data["tagworkingdir"]) for filename in ["setup.py", "pyproject.toml"]): + if any( + filename in os.listdir(self.data["tagworkingdir"]) + for filename in ["setup.py", "pyproject.toml"] + ): self._upload_distributions(package) # Make sure we are in the expected directory again. diff --git a/zest/releaser/tests/baserelease.txt b/zest/releaser/tests/baserelease.txt index 7911fff6..6728b904 100644 --- a/zest/releaser/tests/baserelease.txt +++ b/zest/releaser/tests/baserelease.txt @@ -153,4 +153,4 @@ history file ends with ``.md``, we consider it Markdown:: >>> base = baserelease.Basereleaser() >>> base._grab_history() >>> base.history_format - 'md' \ No newline at end of file + 'md' diff --git a/zest/releaser/tests/cmd_error.py b/zest/releaser/tests/cmd_error.py index 05b033fc..4f6a3e2a 100644 --- a/zest/releaser/tests/cmd_error.py +++ b/zest/releaser/tests/cmd_error.py @@ -1,4 +1,5 @@ # Python script to test some corner cases that print warnings to stderr. import sys + print(sys.argv[1], file=sys.stderr) diff --git a/zest/releaser/tests/functional.py b/zest/releaser/tests/functional.py index 06e6ac9e..6d2375d3 100644 --- a/zest/releaser/tests/functional.py +++ b/zest/releaser/tests/functional.py @@ -1,6 +1,5 @@ """Set up functional test fixtures""" -from colorama import Fore from io import StringIO from urllib import request from urllib.error import HTTPError @@ -116,7 +115,7 @@ def rename_changelog(src: str, dst: str): "mock_pypi_available": test.mock_pypi_available, "add_changelog_entry": add_changelog_entry, "commit_all_changes": commit_all_changes, - "rename_changelog": rename_changelog + "rename_changelog": rename_changelog, } ) diff --git a/zest/releaser/tests/test_setup.py b/zest/releaser/tests/test_setup.py index 2d64d22c..f12fe244 100644 --- a/zest/releaser/tests/test_setup.py +++ b/zest/releaser/tests/test_setup.py @@ -55,7 +55,6 @@ def mock_dispatch(*args): ) - def test_suite(): """Find .txt files and test code examples in them.""" suite = unittest.TestSuite() diff --git a/zest/releaser/utils.py b/zest/releaser/utils.py index 2adaf8c3..63dad11c 100644 --- a/zest/releaser/utils.py +++ b/zest/releaser/utils.py @@ -79,7 +79,9 @@ def read_text_file(filename, encoding=None, fallback_encoding=None): if fallback_encoding: logger.debug( - "Decoding file %s from encoding %s from setup.cfg.", filename, fallback_encoding + "Decoding file %s from encoding %s from setup.cfg.", + filename, + fallback_encoding, ) try: with open(filename, "rb", encoding=fallback_encoding) as filehandler: @@ -100,9 +102,7 @@ def read_text_file(filename, encoding=None, fallback_encoding=None): with tokenize.open(filename) as filehandler: data = filehandler.read() encoding = filehandler.encoding - logger.debug( - "Detected encoding of %s with tokenize: %s", filename, encoding - ) + logger.debug("Detected encoding of %s with tokenize: %s", filename, encoding) return splitlines_with_trailing(data), encoding @@ -290,7 +290,7 @@ def get_next_answer(self): if self.answers: return self.answers.pop(0) # Accept the default. - return '' + return "" test_answer_book = AnswerBook() @@ -988,7 +988,7 @@ def history_format(config_value, history_file): def string_to_bool(value): """Reimplementation of configparser.ConfigParser.getboolean()""" if value.isalpha(): - value=value.lower() + value = value.lower() if value in ["1", "yes", "true", "on"]: return True elif value in ["0", "no", "false", "off"]: @@ -998,31 +998,31 @@ def string_to_bool(value): def extract_zestreleaser_configparser(config, config_filename): - if not config: - return None + if not config: + return None - try: - result = dict(config["zest.releaser"].items()) - except KeyError: - logger.debug(f"No [zest.releaser] section found in the {config_filename}") - return None - - boolean_keys = [ - "release", - "create-wheel", - "no-input", - "register", - "push-changes", - "less-zeroes", - "tag-signing", - "run-pre-commit", - ] - integer_keys = [ - "version-levels", - ] - for key, value in result.items(): - if key in boolean_keys: - result[key] = string_to_bool(value) - if key in integer_keys: - result[key] = int(value) - return result + try: + result = dict(config["zest.releaser"].items()) + except KeyError: + logger.debug(f"No [zest.releaser] section found in the {config_filename}") + return None + + boolean_keys = [ + "release", + "create-wheel", + "no-input", + "register", + "push-changes", + "less-zeroes", + "tag-signing", + "run-pre-commit", + ] + integer_keys = [ + "version-levels", + ] + for key, value in result.items(): + if key in boolean_keys: + result[key] = string_to_bool(value) + if key in integer_keys: + result[key] = int(value) + return result diff --git a/zest/releaser/vcs.py b/zest/releaser/vcs.py index b184caae..7fec7bd0 100644 --- a/zest/releaser/vcs.py +++ b/zest/releaser/vcs.py @@ -1,3 +1,4 @@ +from configparser import ConfigParser from zest.releaser import pypi from zest.releaser import utils @@ -6,7 +7,6 @@ import re import sys -from configparser import ConfigParser try: # Python 3.11+ From 464370b3f3b425db9443e505a70fc93b53226eb0 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 12 Jul 2023 16:07:04 +0200 Subject: [PATCH 3/5] Noting pre-commit addition --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bce7bc9f..90efb586 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,8 @@ Changelog for zest.releaser - Zest.releaser's settings can now also be placed in ``pyproject.toml``. +- Added pre-commit config for neater code (black, flake8, isort). + 8.0.0 (2023-05-05) ------------------ From 902522dd5f962e373102eee023cc5c65b1845c71 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 12 Jul 2023 16:08:50 +0200 Subject: [PATCH 4/5] Adding a separate lint action --- .github/workflows/lint.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..7cbf21ed --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: Lint + +on: + push: + branches: + - master + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Run black, flake8, isort + uses: pre-commit/action@v3.0.0 From b7fa0da75beb9c536f48fe2b215db534787384aa Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 12 Jul 2023 16:14:07 +0200 Subject: [PATCH 5/5] Documented pre-commit --- doc/source/developing.rst | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/doc/source/developing.rst b/doc/source/developing.rst index a7b5851c..594cf202 100644 --- a/doc/source/developing.rst +++ b/doc/source/developing.rst @@ -41,17 +41,31 @@ Running tests Actually, this should be easy now, because we use tox. So ``pip install tox`` somewhere, probably in a virtualenv, maybe the current directory, -and call it: +and call it:: - tox + $ tox You probably want to run the tests for all environments in parallel:: - tox -p auto + $ tox -p auto To run a specific environment and a specific test file:: - tox -e py38 -- utils.txt + $ tox -e py38 -- utils.txt + + +Code formatting +--------------- + +We use black/flake8/isort. To make it easy to configure and run, there's a +pre-commit config. Enable it with:: + + $ pre-commit install + +That will run it before every commit. You can also run it periodically when +developing:: + + $ pre-commit run --all Python versions @@ -64,15 +78,14 @@ Necessary programs ------------------ To run the tests, you need to have the supported versioning systems installed. -Since version 7, we only support ``git``. -On ubuntu:: - - $ sudo apt-get install git +Since version 7, we only support ``git``, which you already have installed +probably :-) There may be test failures when you have different versions of these programs. -In that case, please investigate as these may be genuine errors. -In the past, ``git`` commands would give slightly different output. -If the output of a command changes again, we may need extra compatibility code in ``test_setup.py`` +In that case, please investigate as these *may* be genuine errors. In the +past, ``git`` commands would give slightly different output. If the output of +a command changes again, we may need extra compatibility code in +``test_setup.py``. Building the documentation locally