From ec81338c1a098e02869db1bbc5509f765495a9f7 Mon Sep 17 00:00:00 2001 From: jenisys Date: Thu, 26 Sep 2024 23:55:19 +0200 Subject: [PATCH] CORRECT-VERSION: 0.6.1 (was: 0.4.1) * Add "pyproject.toml", will be needed in the future. * tasks: Use invoke-cleanup from repo (instead of using a copy) * tasks: Remove old stuff --- CHANGELOG.md | 1 + python/.envrc.use_pep0582.disabled | 25 -- python/cucumber_tag_expressions/__init__.py | 2 +- python/py.requirements/basic.txt | 1 + python/pyproject.toml | 145 +++++++ python/setup.py | 33 +- python/tasks/__init__.py | 10 +- python/tasks/__main__.py | 70 --- python/tasks/_dry_run.py | 44 -- python/tasks/_path.py | 32 ++ python/tasks/invoke_cleanup.py | 447 -------------------- python/tasks/py.requirements.txt | 2 + python/tasks/test.py | 4 +- python/tox.ini | 2 +- 14 files changed, 210 insertions(+), 608 deletions(-) delete mode 100644 python/.envrc.use_pep0582.disabled create mode 100644 python/pyproject.toml delete mode 100644 python/tasks/__main__.py delete mode 100644 python/tasks/_dry_run.py create mode 100644 python/tasks/_path.py delete mode 100644 python/tasks/invoke_cleanup.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4814db..bacbcf69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Changed - [Python] Reuse the action cucumber/action-publish-pypi in release ([#147](https://github.com/cucumber/tag-expressions/pull/147)) +- [Python] Correct version to `6.1.0` (was: `4.1.0`) - [Ruby] Fixed up remaining simple cops and began to reduce complexity of code ([#158](https://github.com/cucumber/tag-expressions/pull/158)) ## [6.1.0] - 2024-01-10 diff --git a/python/.envrc.use_pep0582.disabled b/python/.envrc.use_pep0582.disabled deleted file mode 100644 index b747b8ba..00000000 --- a/python/.envrc.use_pep0582.disabled +++ /dev/null @@ -1,25 +0,0 @@ -# =========================================================================== -# PROJECT ENVIRONMENT SETUP: tag-expressions/python/.envrc.use_pep0582 -# =========================================================================== -# DESCRIPTION: -# Setup Python search path to use the PEP-0582 sub-directory tree. -# -# SEE ALSO: -# * https://direnv.net/ -# * https://peps.python.org/pep-0582/ Python local packages directory -# =========================================================================== - -if [ -z "${PYTHON_VERSION}" ]; then - # -- AUTO-DETECT: Default Python3 version - # EXAMPLE: export PYTHON_VERSION="3.9" - export PYTHON_VERSION=$(python3 -c "import sys; print('.'.join([str(x) for x in sys.version_info[:2]]))") -fi -echo "USE: PYTHON_VERSION=${PYTHON_VERSION}" - -# -- HINT: Support PEP-0582 Python local packages directory (supported by: pdm) -path_add PATH __pypackages__/${PYTHON_VERSION}/bin -path_add PYTHONPATH __pypackages__/${PYTHON_VERSION}/lib - -# -- SIMILAR-TO: -# export PATH="${HERE}/__pypackages__/${PYTHON_VERSION}/bin:${PATH}" -# export PYTHONPATH="${HERE}:${HERE}/__pypackages__/${PYTHON_VERSION}/lib:${PYTHONPATH}" diff --git a/python/cucumber_tag_expressions/__init__.py b/python/cucumber_tag_expressions/__init__.py index 4db3110d..75bb90a1 100644 --- a/python/cucumber_tag_expressions/__init__.py +++ b/python/cucumber_tag_expressions/__init__.py @@ -18,4 +18,4 @@ from __future__ import absolute_import from .parser import parse, TagExpressionParser, TagExpressionError -__version__ = "4.1.0" +__version__ = "6.1.0" diff --git a/python/py.requirements/basic.txt b/python/py.requirements/basic.txt index 8ced1bde..c797cbe6 100644 --- a/python/py.requirements/basic.txt +++ b/python/py.requirements/basic.txt @@ -9,3 +9,4 @@ # ============================================================================ enum34; python_version < '3.4' +setuptools diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 00000000..0cb32663 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,145 @@ +# ============================================================================= +# PACKAGING: cucumber-tag-expressions +# ============================================================================= +# SEE: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html +# SEE: https://pypi.org/classifiers/ +# MAYBE: requires = ["setuptools", "setuptools-scm"] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + + +[project] +name = "cucumber-tag-expressions" +dynamic = ["version"] +description = "Provides a tag-expression parser and evaluation logic for cucumber/behave" +readme = "README.rst" +authors = [ + { name="Jens Engel", email="jenisys@users.noreply.github.com" }, +] +dependencies = [ + "enum34; python_version < '3.4'", +] +keywords= ["BDD", "testing", "tag-expressions", "cucumber", "behave"] +# source-includes = [ +# "tests/", +# "py.requirements/", +# ".bumbversion.cfg", +# ".coveragerc", +# ".editorconfig", +# ".envrc", +# ".pylintrc", +# "LICENSE", +# "Makefile", +# "**/*.cfg", +# "**/*.in", +# "**/*.ini", +# "**/*.mk", +# "**/*.toml", +# "**/*.yaml", +# "**/*.yml", +# "**/*.rst", +# "**/*.txt", +# ] +requires-python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +# requires-python = ">=3.7" +license = { text = "MIT"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Testing :: Acceptance", + "Topic :: Software Development :: Testing :: BDD", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", +] + + +[project.urls] +homepage = "https://github.com/cucumber/tag-expressions" +repository = "https://github.com/cucumber/tag-expressions" +download = "https://pypi.org/project/cucumber-tag-expressions" +documentation = "https://cucumber.io/docs/cucumber/api/#tag-expressions" +changelog = "https://github.com/cucumber/tag-expressions/blob/main/CHANGELOG.md" +"Issue Tracker" = "https://github.com/cucumber/tag-expressions/issues/" + + +[project.optional-dependencies] +testing = [ + "pytest < 5.0; python_version < '3.0'", # >= 4.2 + "pytest >= 5.0; python_version >= '3.0'", + "pytest-html >= 1.19.0,<2.0; python_version < '3.0'", + "pytest-html >= 2.0; python_version >= '3.0'", +] +develop = [ + "pytest < 5.0; python_version < '3.0'", # >= 4.2 + "pytest >= 5.0; python_version >= '3.0'", + "pytest-html >= 1.19.0", + "pytest-cov", + "coverage >= 5.0.3", + "tox >=4.20,<4.21", + "pylint", + "ruff", + + # -- INVOKE SUPPORT: ---------------------------------- + # HINT: path.py => path (python-install-package was renamed for python3) + "invoke >= 1.4.1", + "six >= 1.15.0", + "invoke-cleanup @ git+https://github.com/jenisys/invoke-cleanup@v0.3.7", + "path >= 13.1.0; python_version >= '3.5'", + "path.py == 11.5.2; python_version < '3.5'", + + # -- PYTHON2 BACKPORTS: + "pathlib; python_version <= '3.4'", + "backports.shutil_which; python_version <= '3.3'", + # -- CLEANUP SUPPORT: py.cleanup + "pycmd", +] + + +[tool.distutils.bdist_wheel] +universal = true + + +# ----------------------------------------------------------------------------- +# PACAKING TOOL SPECIFIC PARTS: +# ----------------------------------------------------------------------------- +[tool.setuptools] +platforms = ["any"] +zip-safe = true + +# -- PREPARED: +[tool.setuptools.dynamic] +version = {attr = "cucumber_tag_expressions.__version__"} + +[tool.setuptools.packages.find] +where = ["."] +include = ["cucumber_tag_expressions*"] +exclude = ["tests*"] +namespaces = false + + +# ============================================================================= +# OTHER TOOLS +# ============================================================================= +[tool.pylint.messages_control] +disable = "C0330, C0326" + +[tool.pylint.format] +max-line-length = "100" diff --git a/python/setup.py b/python/setup.py index 1cd31efe..bb3fc9d0 100644 --- a/python/setup.py +++ b/python/setup.py @@ -63,25 +63,26 @@ def find_packages_by_root_package(where): include_package_data = True, # -- REQUIREMENTS: - python_requires=">=2.7", + # SUPPORT: python2.7, python3.5 (or higher) + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", install_requires=[ "enum34; python_version < '3.4'" ], - tests_require=[ - "pytest <8.4; python_version < '3.0'", - "pytest >= 5.0; python_version >= '3.0'", - "pytest-html >=4,<4.2; python_version < '3.0'", - "pytest-html >= 2.0; python_version >= '3.0'", - "PyYAML >= 5.4.1", - "pathlib; python_version <= '3.4'", - ], + # tests_require=[ + # "pytest < 5.0; python_version < '3.0'", + # "pytest >= 5.0; python_version >= '3.0'", + # "pytest-html >= 1.19.0,<2.0; python_version < '3.0'", + # "pytest-html >= 2.0; python_version >= '3.0'", + # "PyYAML >= 5.4.1", + # "pathlib; python_version <= '3.4'", + # ], extras_require={ # PREPARED: 'docs': ["sphinx>=1.5"], "develop": [ "coverage", - "pytest <8.4; python_version < '3.0'", + "pytest < 5.0; python_version < '3.0'", "pytest >= 5.0; python_version >= '3.0'", - "pytest-html >=4,<4.2; python_version < '3.0'", + "pytest-html >= 1.19.0,<2.0; python_version < '3.0'", "pytest-html >= 2.0; python_version >= '3.0'", "tox >=4.20,<4.21", "pylint", @@ -89,6 +90,7 @@ def find_packages_by_root_package(where): # -- INVOKE SUPPORT: "invoke >= 1.7.3", "six >= 1.16.0", + "invoke-cleanup @ git+https://github.com/jenisys/invoke-cleanup@v0.3.7", "path >= 13.1.0; python_version >= '3.5'", "path.py >= 11.5.0; python_version < '3.5'", # PYTHON2 BACKPORTS: @@ -98,10 +100,8 @@ def find_packages_by_root_package(where): "pycmd", ], }, - - test_suite = "tests", + # test_suite = "tests", zip_safe = True, - classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", @@ -113,6 +113,11 @@ def find_packages_by_root_package(where): "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", diff --git a/python/tasks/__init__.py b/python/tasks/__init__.py index f3cf8cf2..a21e9cd1 100644 --- a/python/tasks/__init__.py +++ b/python/tasks/__init__.py @@ -20,6 +20,7 @@ from . import _setup # pylint: disable=wrong-import-order import os.path import sys + INVOKE_MINVERSION = "1.2.0" _setup.setup_path() _setup.require_invoke_minversion(INVOKE_MINVERSION) @@ -28,6 +29,11 @@ TOPDIR = os.path.abspath(TOPDIR) sys.path.insert(0, TOPDIR) +# -- MONKEYPATCH: path module +from ._path import monkeypatch_path_if_needed +monkeypatch_path_if_needed() + + # ----------------------------------------------------------------------------- # IMPORTS: # ----------------------------------------------------------------------------- @@ -35,9 +41,8 @@ from invoke import Collection # -- TASK-LIBRARY: -from . import invoke_cleanup as cleanup +import invoke_cleanup as cleanup from . import test -from . import release # ----------------------------------------------------------------------------- # TASKS: @@ -51,7 +56,6 @@ namespace = Collection() namespace.add_collection(Collection.from_module(cleanup), name="cleanup") namespace.add_collection(Collection.from_module(test)) -namespace.add_collection(Collection.from_module(release)) cleanup.cleanup_tasks.add_task(cleanup.clean_python) diff --git a/python/tasks/__main__.py b/python/tasks/__main__.py deleted file mode 100644 index 637d8412..00000000 --- a/python/tasks/__main__.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- coding: UTF-8 -*- -""" -Provides "invoke" script when invoke is not installed. -Note that this approach uses the "tasks/_vendor/invoke.zip" bundle package. - -Usage:: - - # -- INSTEAD OF: invoke command - # Show invoke version - python -m tasks --version - - # List all tasks - python -m tasks -l - -.. seealso:: - - * http://pyinvoke.org - * https://github.com/pyinvoke/invoke - - -Examples for Invoke Scripts using the Bundle -------------------------------------------------------------------------------- - -For UNIX like platforms: - -.. code-block:: sh - - #!/bin/sh - #!/bin/bash - # RUN INVOKE: From bundled ZIP file (with Bourne shell/bash script). - # FILE: invoke.sh (in directory that contains tasks/ directory) - - HERE=$(dirname $0) - export INVOKE_TASKS_USE_VENDOR_BUNDLES="yes" - - python ${HERE}/tasks/_vendor/invoke.zip $* - - -For Windows platform: - -.. code-block:: bat - - @echo off - REM RUN INVOKE: From bundled ZIP file (with Windows Batchfile). - REM FILE: invoke.cmd (in directory that contains tasks/ directory) - - setlocal - set HERE=%~dp0 - set INVOKE_TASKS_USE_VENDOR_BUNDLES="yes" - if not defined PYTHON set PYTHON=python - - %PYTHON% %HERE%tasks/_vendor/invoke.zip "%*" -""" - -from __future__ import absolute_import -import os -import sys - -# ----------------------------------------------------------------------------- -# BOOTSTRAP PATH: Use provided vendor bundle if "invoke" is not installed -# ----------------------------------------------------------------------------- -# NOTE: tasks/__init__.py performs sys.path setup. -os.environ["INVOKE_TASKS_USE_VENDOR_BUNDLES"] = "yes" - -# ----------------------------------------------------------------------------- -# AUTO-MAIN: -# ----------------------------------------------------------------------------- -if __name__ == "__main__": - from invoke.main import program - sys.exit(program.run()) diff --git a/python/tasks/_dry_run.py b/python/tasks/_dry_run.py deleted file mode 100644 index cedbdd4d..00000000 --- a/python/tasks/_dry_run.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: UTF-8 -*- -""" -Basic support to use a --dry-run mode w/ invoke tasks. - -.. code-block:: - - from ._dry_run import DryRunContext - - @task - def destroy_something(ctx, path, dry_run=False): - if dry_run: - ctx = DryRunContext(ctx) - - # -- DRY-RUN MODE: Only echos commands. - ctx.run("rm -rf {}".format(path)) -""" - -from __future__ import print_function - -class DryRunContext(object): - PREFIX = "DRY-RUN: " - SCHEMA = "{prefix}{command}" - SCHEMA_WITH_KWARGS = "{prefix}{command} (with kwargs={kwargs})" - - def __init__(self, ctx=None, prefix=None, schema=None): - if prefix is None: - prefix = self.PREFIX - if schema is None: - schema = self.SCHEMA - - self.ctx = ctx - self.prefix = prefix - self.schema = schema - - def run(self, command, **kwargs): - message = self.schema.format(command=command, - prefix=self.prefix, - kwargs=kwargs) - print(message) - - - def sudo(self, command, **kwargs): - command2 = "sudo %s" % command - self.run(command2, **kwargs) diff --git a/python/tasks/_path.py b/python/tasks/_path.py new file mode 100644 index 00000000..cb03d9f4 --- /dev/null +++ b/python/tasks/_path.py @@ -0,0 +1,32 @@ +""" +Fixes :mod:`path` breaking API changes. + +Newer versions of :mod:`path` no longer support: + +* :func:`Path.abspath()`: Use :func:`Path.absolute()` +* :func:`Path.isdir()`: Use :func:`Path.is_dir()` +* :func:`Path.isfile()`: Use :func:`Path.is_file()` +* ... + +.. seealso:: https://github.com/jaraco/path +""" + +from path import Path + + +# ----------------------------------------------------------------------------- +# MONKEYPATCH (if needed) +# ----------------------------------------------------------------------------- +def monkeypatch_path_if_needed(): + if not hasattr(Path, "abspath"): + Path.abspath = Path.absolute + if not hasattr(Path, "isdir"): + Path.isdir = Path.is_dir + if not hasattr(Path, "isfile"): + Path.isfile = Path.is_file + + +# ----------------------------------------------------------------------------- +# MODULE SETUP +# ----------------------------------------------------------------------------- +# DISABLED: monkeypatch_path_if_needed() diff --git a/python/tasks/invoke_cleanup.py b/python/tasks/invoke_cleanup.py deleted file mode 100644 index 4e631c43..00000000 --- a/python/tasks/invoke_cleanup.py +++ /dev/null @@ -1,447 +0,0 @@ -# -*- coding: UTF-8 -*- -""" -Provides cleanup tasks for invoke build scripts (as generic invoke tasklet). -Simplifies writing common, composable and extendable cleanup tasks. - -PYTHON PACKAGE DEPENDENCIES: - -* path (python >= 3.5) or path.py >= 11.5.0 (as path-object abstraction) -* pathlib (for ant-like wildcard patterns; since: python > 3.5) -* pycmd (required-by: clean_python()) - - -cleanup task: Add Additional Directories and Files to be removed -------------------------------------------------------------------------------- - -Create an invoke configuration file (YAML of JSON) with the additional -configuration data: - -.. code-block:: yaml - - # -- FILE: invoke.yaml - # USE: cleanup.directories, cleanup.files to override current configuration. - cleanup: - # directories: Default directory patterns (can be overwritten). - # files: Default file patterns (can be ovewritten). - extra_directories: - - **/tmp/ - extra_files: - - **/*.log - - **/*.bak - - -Registration of Cleanup Tasks ------------------------------- - -Other task modules often have an own cleanup task to recover the clean state. -The :meth:`cleanup` task, that is provided here, supports the registration -of additional cleanup tasks. Therefore, when the :meth:`cleanup` task is executed, -all registered cleanup tasks will be executed. - -EXAMPLE:: - - # -- FILE: tasks/docs.py - from __future__ import absolute_import - from invoke import task, Collection - from invoke_cleanup import cleanup_tasks, cleanup_dirs - - @task - def clean(ctx): - "Cleanup generated documentation artifacts." - dry_run = ctx.config.run.dry - cleanup_dirs(["build/docs"], dry_run=dry_run) - - namespace = Collection(clean) - ... - - # -- REGISTER CLEANUP TASK: - cleanup_tasks.add_task(clean, "clean_docs") - cleanup_tasks.configure(namespace.configuration()) -""" - -from __future__ import absolute_import, print_function -import os -import sys -from invoke import task, Collection -from invoke.executor import Executor -from invoke.exceptions import Exit, Failure, UnexpectedExit -from invoke.util import cd -from path import Path - -# -- PYTHON BACKWARD COMPATIBILITY: -python_version = sys.version_info[:2] -python35 = (3, 5) # HINT: python3.8 does not raise OSErrors. -if python_version < python35: # noqa - import pathlib2 as pathlib -else: - import pathlib # noqa - - -# ----------------------------------------------------------------------------- -# CONSTANTS: -# ----------------------------------------------------------------------------- -VERSION = "0.3.6" - - -# ----------------------------------------------------------------------------- -# CLEANUP UTILITIES: -# ----------------------------------------------------------------------------- -def execute_cleanup_tasks(ctx, cleanup_tasks, workdir=".", verbose=False): - """Execute several cleanup tasks as part of the cleanup. - - :param ctx: Context object for the tasks. - :param cleanup_tasks: Collection of cleanup tasks (as Collection). - """ - # pylint: disable=redefined-outer-name - executor = Executor(cleanup_tasks, ctx.config) - failure_count = 0 - with cd(workdir) as cwd: - for cleanup_task in cleanup_tasks.tasks: - try: - print("CLEANUP TASK: %s" % cleanup_task) - executor.execute(cleanup_task) - except (Exit, Failure, UnexpectedExit) as e: - print(e) - print("FAILURE in CLEANUP TASK: %s (GRACEFULLY-IGNORED)" % cleanup_task) - failure_count += 1 - - if failure_count: - print("CLEANUP TASKS: %d failure(s) occured" % failure_count) - - -def make_excluded(excluded, config_dir=None, workdir=None): - workdir = workdir or Path.getcwd() - config_dir = config_dir or workdir - workdir = Path(workdir) - config_dir = Path(config_dir) - - excluded2 = [] - for p in excluded: - assert p, "REQUIRE: non-empty" - p = Path(p) - if p.isabs(): - excluded2.append(p.normpath()) - else: - # -- RELATIVE PATH: - # Described relative to config_dir. - # Recompute it relative to current workdir. - p = Path(config_dir)/p - p = workdir.relpathto(p) - excluded2.append(p.normpath()) - excluded2.append(p.abspath()) - return set(excluded2) - - -def is_directory_excluded(directory, excluded): - directory = Path(directory).normpath() - directory2 = directory.abspath() - if (directory in excluded) or (directory2 in excluded): - return True - # -- OTHERWISE: - return False - - -def cleanup_dirs(patterns, workdir=".", excluded=None, - dry_run=False, verbose=False, show_skipped=False): - """Remove directories (and their contents) recursively. - Skips removal if directories does not exist. - - :param patterns: Directory name patterns, like "**/tmp*" (as list). - :param workdir: Current work directory (default=".") - :param dry_run: Dry-run mode indicator (as bool). - """ - excluded = excluded or [] - excluded = set([Path(p) for p in excluded]) - show_skipped = show_skipped or verbose - current_dir = Path(workdir) - python_basedir = Path(Path(sys.executable).dirname()).joinpath("..").abspath() - warn2_counter = 0 - for dir_pattern in patterns: - for directory in path_glob(dir_pattern, current_dir): - if is_directory_excluded(directory, excluded): - print("SKIP-DIR: %s (excluded)" % directory) - continue - directory2 = directory.abspath() - if sys.executable.startswith(directory2): - # -- PROTECT VIRTUAL ENVIRONMENT (currently in use): - # pylint: disable=line-too-long - print("SKIP-SUICIDE: '%s' contains current python executable" % directory) - continue - elif directory2.startswith(python_basedir): - # -- PROTECT VIRTUAL ENVIRONMENT (currently in use): - # HINT: Limit noise in DIAGNOSTIC OUTPUT to X messages. - if warn2_counter <= 4: # noqa - print("SKIP-SUICIDE: '%s'" % directory) - warn2_counter += 1 - continue - - if not directory.isdir(): - if show_skipped: - print("RMTREE: %s (SKIPPED: Not a directory)" % directory) - continue - - if dry_run: - print("RMTREE: %s (dry-run)" % directory) - else: - try: - # -- MAYBE: directory.rmtree(ignore_errors=True) - print("RMTREE: %s" % directory) - directory.rmtree_p() - except OSError as e: - print("RMTREE-FAILED: %s (for: %s)" % (e, directory)) - - -def cleanup_files(patterns, workdir=".", dry_run=False, verbose=False, show_skipped=False): - """Remove files or files selected by file patterns. - Skips removal if file does not exist. - - :param patterns: File patterns, like "**/*.pyc" (as list). - :param workdir: Current work directory (default=".") - :param dry_run: Dry-run mode indicator (as bool). - """ - show_skipped = show_skipped or verbose - current_dir = Path(workdir) - python_basedir = Path(Path(sys.executable).dirname()).joinpath("..").abspath() - error_message = None - error_count = 0 - for file_pattern in patterns: - for file_ in path_glob(file_pattern, current_dir): - if file_.abspath().startswith(python_basedir): - # -- PROTECT VIRTUAL ENVIRONMENT (currently in use): - continue - if not file_.isfile(): - if show_skipped: - print("REMOVE: %s (SKIPPED: Not a file)" % file_) - continue - - if dry_run: - print("REMOVE: %s (dry-run)" % file_) - else: - print("REMOVE: %s" % file_) - try: - file_.remove_p() - except os.error as e: - message = "%s: %s" % (e.__class__.__name__, e) - print(message + " basedir: "+ python_basedir) - error_count += 1 - if not error_message: - error_message = message - if False and error_message: # noqa - class CleanupError(RuntimeError): - pass - raise CleanupError(error_message) - - -def path_glob(pattern, current_dir=None): - """Use pathlib for ant-like patterns, like: "**/*.py" - - :param pattern: File/directory pattern to use (as string). - :param current_dir: Current working directory (as Path, pathlib.Path, str) - :return Resolved Path (as path.Path). - """ - if not current_dir: # noqa - current_dir = pathlib.Path.cwd() - elif not isinstance(current_dir, pathlib.Path): - # -- CASE: string, path.Path (string-like) - current_dir = pathlib.Path(str(current_dir)) - - pattern_path = Path(pattern) - if pattern_path.isabs(): - # -- SPECIAL CASE: Path.glob() only supports relative-path(s) / pattern(s). - if pattern_path.isdir(): - yield pattern_path - return - - # -- HINT: OSError is no longer raised in pathlib2 or python35.pathlib - # try: - for p in current_dir.glob(pattern): - yield Path(str(p)) - # except OSError as e: - # # -- CORNER-CASE 1: x.glob(pattern) may fail with: - # # OSError: [Errno 13] Permission denied: - # # HINT: Directory lacks excutable permissions for traversal. - # # -- CORNER-CASE 2: symlinked endless loop - # # OSError: [Errno 62] Too many levels of symbolic links: - # print("{0}: {1}".format(e.__class__.__name__, e)) - - -# ----------------------------------------------------------------------------- -# GENERIC CLEANUP TASKS: -# ----------------------------------------------------------------------------- -@task(help={ - "workdir": "Directory to clean(up) (default: $CWD).", - "verbose": "Enable verbose mode (default: OFF).", -}) -def clean(ctx, workdir=".", verbose=False): - """Cleanup temporary dirs/files to regain a clean state.""" - dry_run = ctx.config.run.dry - config_dir = getattr(ctx.config, "config_dir", workdir) - directories = list(ctx.config.cleanup.directories or []) - directories.extend(ctx.config.cleanup.extra_directories or []) - files = list(ctx.config.cleanup.files or []) - files.extend(ctx.config.cleanup.extra_files or []) - excluded_directories = list(ctx.config.cleanup.excluded_directories or []) - excluded_directories = make_excluded(excluded_directories, - config_dir=config_dir, workdir=".") - - # -- PERFORM CLEANUP: - execute_cleanup_tasks(ctx, cleanup_tasks) - cleanup_dirs(directories, workdir=workdir, excluded=excluded_directories, - dry_run=dry_run, verbose=verbose) - cleanup_files(files, workdir=workdir, dry_run=dry_run, verbose=verbose) - - # -- CONFIGURABLE EXTENSION-POINT: - # use_cleanup_python = ctx.config.cleanup.use_cleanup_python or False - # if use_cleanup_python: - # clean_python(ctx) - - -@task(name="all", aliases=("distclean",), - help={ - "workdir": "Directory to clean(up) (default: $CWD).", - "verbose": "Enable verbose mode (default: OFF).", -}) -def clean_all(ctx, workdir=".", verbose=False): - """Clean up everything, even the precious stuff. - NOTE: clean task is executed last. - """ - dry_run = ctx.config.run.dry - config_dir = getattr(ctx.config, "config_dir", workdir) - directories = list(ctx.config.cleanup_all.directories or []) - directories.extend(ctx.config.cleanup_all.extra_directories or []) - files = list(ctx.config.cleanup_all.files or []) - files.extend(ctx.config.cleanup_all.extra_files or []) - excluded_directories = list(ctx.config.cleanup_all.excluded_directories or []) - excluded_directories.extend(ctx.config.cleanup.excluded_directories or []) - excluded_directories = make_excluded(excluded_directories, - config_dir=config_dir, workdir=".") - - # -- PERFORM CLEANUP: - # HINT: Remove now directories, files first before cleanup-tasks. - cleanup_dirs(directories, workdir=workdir, excluded=excluded_directories, - dry_run=dry_run, verbose=verbose) - cleanup_files(files, workdir=workdir, dry_run=dry_run, verbose=verbose) - execute_cleanup_tasks(ctx, cleanup_all_tasks) - clean(ctx, workdir=workdir, verbose=verbose) - - # -- CONFIGURABLE EXTENSION-POINT: - # use_cleanup_python1 = ctx.config.cleanup.use_cleanup_python or False - # use_cleanup_python2 = ctx.config.cleanup_all.use_cleanup_python or False - # if use_cleanup_python2 and not use_cleanup_python1: - # clean_python(ctx) - - -@task(aliases=["python"]) -def clean_python(ctx, workdir=".", verbose=False): - """Cleanup python related files/dirs: *.pyc, *.pyo, ...""" - dry_run = ctx.config.run.dry or False - # MAYBE NOT: "**/__pycache__" - cleanup_dirs(["build", "dist", "*.egg-info", "**/__pycache__"], - workdir=workdir, dry_run=dry_run, verbose=verbose) - if not dry_run: - ctx.run("py.cleanup") - cleanup_files(["**/*.pyc", "**/*.pyo", "**/*$py.class"], - workdir=workdir, dry_run=dry_run, verbose=verbose) - - -@task(help={ - "path": "Path to cleanup.", - "interactive": "Enable interactive mode.", - "force": "Enable force mode.", - "options": "Additional git-clean options", -}) -def git_clean(ctx, path=None, interactive=False, force=False, - dry_run=False, options=None): - """Perform git-clean command to cleanup the worktree of a git repository. - - BEWARE: This may remove any precious files that are not checked in. - WARNING: DANGEROUS COMMAND. - """ - args = [] - force = force or ctx.config.git_clean.force - path = path or ctx.config.git_clean.path or "." - interactive = interactive or ctx.config.git_clean.interactive - dry_run = dry_run or ctx.config.run.dry or ctx.config.git_clean.dry_run - - if interactive: - args.append("--interactive") - if force: - args.append("--force") - if dry_run: - args.append("--dry-run") - args.append(options or "") - args = " ".join(args).strip() - - ctx.run("git clean {options} {path}".format(options=args, path=path)) - - -# ----------------------------------------------------------------------------- -# TASK CONFIGURATION: -# ----------------------------------------------------------------------------- -CLEANUP_EMPTY_CONFIG = { - "directories": [], - "files": [], - "extra_directories": [], - "extra_files": [], - "excluded_directories": [], - "excluded_files": [], - "use_cleanup_python": False, -} -def make_cleanup_config(**kwargs): - config_data = CLEANUP_EMPTY_CONFIG.copy() - config_data.update(kwargs) - return config_data - - -namespace = Collection(clean_all, clean_python) -namespace.add_task(clean, default=True) -namespace.add_task(git_clean) -namespace.configure({ - "cleanup": make_cleanup_config( - files=["**/*.bak", "**/*.log", "**/*.tmp", "**/.DS_Store"], - excluded_directories=[".git", ".hg", ".bzr", ".svn"], - ), - "cleanup_all": make_cleanup_config( - directories=[".venv*", ".tox", "downloads", "tmp"], - ), - "git_clean": { - "interactive": True, - "force": False, - "path": ".", - "dry_run": False, - }, -}) - - -# -- EXTENSION-POINT: CLEANUP TASKS (called by: clean, clean_all task) -# NOTE: Can be used by other tasklets to register cleanup tasks. -cleanup_tasks = Collection("cleanup_tasks") -cleanup_all_tasks = Collection("cleanup_all_tasks") - -# -- EXTEND NORMAL CLEANUP-TASKS: -# DISABLED: cleanup_tasks.add_task(clean_python) - -# ----------------------------------------------------------------------------- -# EXTENSION-POINT: CONFIGURATION HELPERS: Can be used from other task modules -# ----------------------------------------------------------------------------- -def config_add_cleanup_dirs(directories): - # pylint: disable=protected-access - the_cleanup_directories = namespace._configuration["cleanup"]["directories"] - the_cleanup_directories.extend(directories) - -def config_add_cleanup_files(files): - # pylint: disable=protected-access - the_cleanup_files = namespace._configuration["cleanup"]["files"] - the_cleanup_files.extend(files) - # namespace.configure({"cleanup": {"files": files}}) - # print("DIAG cleanup.config.cleanup: %r" % namespace.configuration()) - -def config_add_cleanup_all_dirs(directories): - # pylint: disable=protected-access - the_cleanup_directories = namespace._configuration["cleanup_all"]["directories"] - the_cleanup_directories.extend(directories) - -def config_add_cleanup_all_files(files): - # pylint: disable=protected-access - the_cleanup_files = namespace._configuration["cleanup_all"]["files"] - the_cleanup_files.extend(files) diff --git a/python/tasks/py.requirements.txt b/python/tasks/py.requirements.txt index 32137afe..4b5f7deb 100644 --- a/python/tasks/py.requirements.txt +++ b/python/tasks/py.requirements.txt @@ -11,6 +11,8 @@ invoke >= 1.7.3 six >= 1.16.0 +invoke-cleanup @ git+https://github.com/jenisys/invoke-cleanup@v0.3.7 + # -- HINT: path.py => path (python-install-package was renamed for python3) path >= 13.1.0; python_version >= '3.5' path.py >= 11.5.0; python_version < '3.5' diff --git a/python/tasks/test.py b/python/tasks/test.py index 14a06a8b..cc4063d3 100644 --- a/python/tasks/test.py +++ b/python/tasks/test.py @@ -4,12 +4,10 @@ """ from __future__ import print_function -import os.path -import sys from invoke import task, Collection # -- TASK-LIBRARY: -from .invoke_cleanup import cleanup_tasks, cleanup_dirs, cleanup_files +from invoke_cleanup import cleanup_tasks, cleanup_dirs, cleanup_files # --------------------------------------------------------------------------- diff --git a/python/tox.ini b/python/tox.ini index aab9f2c9..2f6c62e8 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -22,7 +22,7 @@ [tox] minversion = 2.8 -envlist = py311, py310, py27, py39, pypy3 +envlist = py312, py311, py310, py27, pypy3 skip_missing_interpreters = true