diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2bc37b6..46728cb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -14,6 +14,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: "3.12" - uses: pre-commit/action@v3.0.0 build: strategy: diff --git a/.gitignore b/.gitignore index 39470c8..12c4f4e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,5 @@ build/ constraints-mxdev.txt example/*-outfile.txt requirements-mxdev.txt -venv/ +.venv/ dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45891db..cd8e5d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,19 +1,19 @@ --- repos: - repo: https://github.com/psf/black.git - rev: 22.3.0 + rev: 24.2.0 hooks: - id: black language_version: python3 exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.960' # Use the sha / tag you want to point at + rev: 'v1.9.0' # Use the sha / tag you want to point at hooks: - id: mypy additional_dependencies: [types-setuptools] - repo: https://github.com/PyCQA/doc8 - rev: 0.8.1 + rev: v1.1.1 hooks: - id: doc8 name: doc8 @@ -28,13 +28,8 @@ repos: # - id: flake8 # additional_dependencies: # - flake8-docstrings - # - repo: https://github.com/PyCQA/bandit - # rev: 1.6.0 - # hooks: - # - id: bandit - # args: [--ini, .bandit] - repo: https://github.com/mgedmin/check-manifest - rev: "0.48" + rev: "0.49" hooks: - id: check-manifest # - repo: https://github.com/Lucas-C/pre-commit-hooks-safety diff --git a/CHANGES.md b/CHANGES.md index bd280b8..99db67a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,9 +2,9 @@ ## 4.0.2 (unreleased) - -- Nothing changed yet. - +- Fix #42: deprecated use of `pkg_resoures` to load entry points and parse requirements. + This enables mxdev to work on Python 3.12, where `pkg_resources` is no longer installed by default in virtual_envs. + [jensens] ## 4.0.1 (2024-03-01) diff --git a/Makefile b/Makefile index 5050cde..af82b2c 100644 --- a/Makefile +++ b/Makefile @@ -81,8 +81,8 @@ VENV_CREATE?=true # target folder for the virtual environment. If `VENV_ENABLED` is `true` and # `VENV_CREATE` is false it is expected to point to an existing virtual # environment. If `VENV_ENABLED` is `false` it is ignored. -# Default: venv -VENV_FOLDER?=venv +# Default: .venv +VENV_FOLDER?=.venv # mxdev to install in virtual environment. # Default: mxdev @@ -222,8 +222,13 @@ endif # Determine the executable path ifeq ("$(VENV_ENABLED)", "true") -export PATH:=$(abspath $(VENV_FOLDER))/bin:$(PATH) export VIRTUAL_ENV=$(abspath $(VENV_FOLDER)) +ifeq ("$(OS)", "Windows_NT") +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/Scripts +else +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/bin +endif +export PATH:=$(VENV_EXECUTABLE_FOLDER):$(PATH) MXENV_PYTHON=python else MXENV_PYTHON=$(PRIMARY_PYTHON) diff --git a/pyproject.toml b/pyproject.toml index 6da73cd..d55863a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,20 +21,16 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -dependencies = [] +dependencies = ["packaging"] dynamic = ["readme"] [project.optional-dependencies] -mypy = [ - "types-setuptools", - "types-pkg-resources", -] +mypy = [] test = [ "pytest", "pytest-cov", "pytest-mock", "httpretty", - "types-setuptools", ] [project.urls] diff --git a/src/mxdev/config.py b/src/mxdev/config.py index bf8e3f7..4b61d9b 100644 --- a/src/mxdev/config.py +++ b/src/mxdev/config.py @@ -1,8 +1,8 @@ from .including import read_with_included from .logging import logger +from packaging.requirements import Requirement import os -import pkg_resources import typing @@ -55,11 +55,11 @@ def __init__( self.overrides = {} for line in raw_overrides.split("\n"): try: - parsed = pkg_resources.Requirement.parse(line) + parsed = Requirement(line) except Exception: logger.error(f"Can not parse override: {line}") continue - self.overrides[parsed.key] = line + self.overrides[parsed.name] = line raw_ignores = settings.get("ignores", "").strip() self.ignore_keys = [] @@ -68,7 +68,7 @@ def __init__( if line: self.ignore_keys.append(line) - def is_ns_member(name): + def is_ns_member(name) -> bool: for hook in hooks: if name.startswith(hook.namespace): return True diff --git a/src/mxdev/entry_points.py b/src/mxdev/entry_points.py new file mode 100644 index 0000000..6d610f4 --- /dev/null +++ b/src/mxdev/entry_points.py @@ -0,0 +1,27 @@ +# this is a helper to load entrypoints with importlib, since pkg_resources +# is deprecated. In Python 3.12 an API incompatible change was introduced, +# so this code is that ugly now. +from importlib.metadata import entry_points + + +try: + # do we have Python 3.12+? + from importlib.metadata import EntryPoints # type: ignore # noqa: F401 + + HAS_IMPORTLIB_ENTRYPOINTS = True +except ImportError: + HAS_IMPORTLIB_ENTRYPOINTS = False + + +def load_eps_by_group(group: str) -> list: + if HAS_IMPORTLIB_ENTRYPOINTS: + eps = entry_points(group=group) # type: ignore + else: + eps_base = entry_points() + if group not in eps_base: + return [] + eps = eps_base[group] # type: ignore + # XXX: for some reasons entry points are loaded twice. not sure if this + # is a glitch when installing with uv or something related to + # importlib.metadata.entry_points + return list(set(eps)) # type: ignore diff --git a/src/mxdev/hooks.py b/src/mxdev/hooks.py index 153a8d7..cfad663 100644 --- a/src/mxdev/hooks.py +++ b/src/mxdev/hooks.py @@ -1,9 +1,18 @@ +from .entry_points import load_eps_by_group from .state import State -from pkg_resources import iter_entry_points import typing +try: + # do we have Python 3.12+ + from importlib.metadata import EntryPoints # type: ignore # noqa: F401 + + HAS_IMPORTLIB_ENTRYPOINTS = True +except ImportError: + HAS_IMPORTLIB_ENTRYPOINTS = False + + class Hook: """Entry point for hooking into mxdev.""" @@ -18,7 +27,7 @@ def write(self, state: State) -> None: def load_hooks() -> list: - return [ep.load()() for ep in iter_entry_points("mxdev") if ep.name == "hook"] + return [ep.load()() for ep in load_eps_by_group("mxdev") if ep.name == "hook"] def read_hooks(state: State, hooks: typing.List[Hook]) -> None: diff --git a/src/mxdev/processing.py b/src/mxdev/processing.py index 13be0a3..00f3d87 100644 --- a/src/mxdev/processing.py +++ b/src/mxdev/processing.py @@ -1,11 +1,11 @@ from .logging import logger from .state import State from .vcs.common import WorkingCopies +from packaging.requirements import Requirement from pathlib import Path from urllib import parse from urllib import request -import pkg_resources import typing @@ -49,15 +49,15 @@ def process_line( variety="r", ) try: - parsed = pkg_resources.Requirement.parse(line) + parsed = Requirement(line) except Exception: pass else: - if parsed.key in package_keys: + if parsed.name in package_keys: line = f"# {line.strip()} -> mxdev disabled (source)\n" - if variety == "c" and parsed.key in override_keys: + if variety == "c" and parsed.name in override_keys: line = f"# {line.strip()} -> mxdev disabled (override)\n" - if variety == "c" and parsed.key in ignore_keys: + if variety == "c" and parsed.name in ignore_keys: line = f"# {line.strip()} -> mxdev disabled (ignore)\n" if variety == "c": return [], [line] diff --git a/src/mxdev/tests/test_common.py b/src/mxdev/tests/test_common.py index 5fa6de9..d96a744 100644 --- a/src/mxdev/tests/test_common.py +++ b/src/mxdev/tests/test_common.py @@ -143,8 +143,7 @@ def test_WorkingCopies_process(mocker, caplog): def test_WorkingCopies_checkout(mocker, caplog, tmpdir): caplog.set_level(logging.INFO) - class SysExit(Exception): - ... + class SysExit(Exception): ... class Exit: def __call__(self, code): diff --git a/src/mxdev/vcs/common.py b/src/mxdev/vcs/common.py index f4a5106..c5aeb2d 100644 --- a/src/mxdev/vcs/common.py +++ b/src/mxdev/vcs/common.py @@ -1,7 +1,8 @@ +from ..entry_points import load_eps_by_group + import abc import logging import os -import pkg_resources import platform import queue import re @@ -93,20 +94,16 @@ def should_update(self, **kwargs) -> bool: return update @abc.abstractmethod - def checkout(self, **kwargs) -> typing.Union[str, None]: - ... + def checkout(self, **kwargs) -> typing.Union[str, None]: ... @abc.abstractmethod - def status(self, **kwargs) -> typing.Union[typing.Tuple[str, str], str]: - ... + def status(self, **kwargs) -> typing.Union[typing.Tuple[str, str], str]: ... @abc.abstractmethod - def matches(self) -> bool: - ... + def matches(self) -> bool: ... @abc.abstractmethod - def update(self, **kwargs) -> typing.Union[str, None]: - ... + def update(self, **kwargs) -> typing.Union[str, None]: ... def yesno( @@ -150,23 +147,16 @@ def get_workingcopytypes() -> typing.Dict[str, typing.Type[BaseWorkingCopy]]: if _workingcopytypes: return _workingcopytypes group = "mxdev.workingcopytypes" - addons = {} - for entrypoint in pkg_resources.iter_entry_points(group=group): + addons: dict[str, typing.Type[BaseWorkingCopy]] = {} + for entrypoint in load_eps_by_group(group): key = entrypoint.name workingcopytype = entrypoint.load() - if not entrypoint.dist: - continue - if entrypoint.dist.project_name == "mxdev": - _workingcopytypes[key] = workingcopytype - continue if key in addons: logger.error( - f"There already is a working copy type addon registered for '{key}'." + f"Duplicate workingcopy types registration '{key}' at " + f"{entrypoint.value} can not override {addons[key]}" ) sys.exit(1) - logger.info( - f"Overwriting '{key}' with addon from '{entrypoint.dist.project_name}'." - ) addons[key] = workingcopytype _workingcopytypes.update(addons) return _workingcopytypes diff --git a/tox.ini b/tox.ini index 993d037..fb4da84 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,8 @@ envlist = py38 py39 py310 + py311 + py312 minversion = 3.25.0 requires = virtualenv >= 20.14.1