From c17077f0f07cdb2594ee51ce5b40dd5d955a6639 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 20 Oct 2023 14:42:59 +0200 Subject: [PATCH 01/17] pkgmeta: Remove use of pkg_resources --- orangecanvas/utils/pkgmeta.py | 91 ++++++++++++------------ orangecanvas/utils/tests/test_pkgmeta.py | 41 +++++++++++ 2 files changed, 86 insertions(+), 46 deletions(-) create mode 100644 orangecanvas/utils/tests/test_pkgmeta.py diff --git a/orangecanvas/utils/pkgmeta.py b/orangecanvas/utils/pkgmeta.py index 4f460e02..de73098b 100644 --- a/orangecanvas/utils/pkgmeta.py +++ b/orangecanvas/utils/pkgmeta.py @@ -1,36 +1,46 @@ import os import sys +import re import email from operator import itemgetter -from typing import TYPE_CHECKING, List, Dict, Optional, Union +from typing import List, Dict, Optional, Union, cast -import pkg_resources +import packaging.version +try: + from importlib.metadata import EntryPoint, Distribution, entry_points +except ImportError: + from importlib_metadata import EntryPoint, Distribution, entry_points -if TYPE_CHECKING: - Distribution = pkg_resources.Distribution +__all__ = [ + "Distribution", "EntryPoint", "entry_points", "normalize_name", "trim", + "trim_leading_lines", "trim_trailing_lines", "parse_meta", "get_dist_meta", +] -def is_develop_egg(dist): - # type: (Distribution) -> bool +def normalize_name(name: str) -> str: + """ + PEP 503 normalization plus dashes as underscores. + """ + return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_') + + +def is_develop_egg(dist: Distribution) -> bool: """ Is the distribution installed in development mode (setup.py develop) """ - meta_provider = dist._provider # type: ignore - egg_info_dir = os.path.dirname(meta_provider.egg_info) - egg_name = pkg_resources.to_filename(dist.project_name) - return meta_provider.egg_info.endswith(egg_name + ".egg-info") \ - and os.path.exists(os.path.join(egg_info_dir, "setup.py")) + egg_info_dir = dist.locate_file(f"{normalize_name(dist.name)}.egg-info/") + setup = dist.locate_file("setup.py") + return os.path.isdir(egg_info_dir) and os.path.isfile(setup) -def left_trim_lines(lines): - # type: (List[str]) -> List[str] +def left_trim_lines(lines: List[str]) -> List[str]: """ Remove all unnecessary leading space from lines. """ lines_striped = zip(lines[1:], map(str.lstrip, lines[1:])) lines_striped = filter(itemgetter(1), lines_striped) - indent = min([len(line) - len(striped) \ + indent = min([len(line) - len(striped) for line, striped in lines_striped] + [sys.maxsize]) if indent < sys.maxsize: @@ -39,8 +49,7 @@ def left_trim_lines(lines): return list(lines) -def trim_trailing_lines(lines): - # type: (List[str]) -> List[str] +def trim_trailing_lines(lines: List[str]) -> List[str]: """ Trim trailing blank lines. """ @@ -50,8 +59,7 @@ def trim_trailing_lines(lines): return lines -def trim_leading_lines(lines): - # type: (List[str]) -> List[str] +def trim_leading_lines(lines: List[str]) -> List[str]: """ Trim leading blank lines. """ @@ -61,15 +69,12 @@ def trim_leading_lines(lines): return lines -def trim(string): - # type: (str) -> str +def trim(string: str) -> str: """ Trim a string in PEP-256 compatible way """ lines = string.expandtabs().splitlines() - lines = list(map(str.lstrip, lines[:1])) + left_trim_lines(lines[1:]) - return "\n".join(trim_leading_lines(trim_trailing_lines(lines))) @@ -79,29 +84,27 @@ def trim(string): "Project-URL"] -def parse_meta(contents): - # type: (str) -> Dict[str, Union[str, List[str]]] +def parse_meta(contents: str) -> Dict[str, Union[str, List[str]]]: message = email.message_from_string(contents) meta = {} # type: Dict[str, Union[str, List[str]]] for key in set(message.keys()): if key in MULTIPLE_KEYS: - meta[key] = list(str(m) for m in message.get_all(key)) + meta[key] = list(str(m) for m in message.get_all(key, [])) else: value = str(message.get(key)) if key == "Description": value = trim(value) meta[key] = value - - version = pkg_resources.parse_version(meta["Metadata-Version"]) - if version >= pkg_resources.parse_version("1.3") and "Description" not in meta: + version_str = cast(str, meta["Metadata-Version"]) + version = packaging.version.parse(version_str) + if version >= packaging.version.parse("1.3") and "Description" not in meta: desc = message.get_payload() if isinstance(desc, str): meta["Description"] = desc return meta -def get_meta_entry(dist, name): - # type: (pkg_resources.Distribution, str) -> Union[List[str], str, None] +def get_meta_entry(dist: Distribution, name: str) -> Union[List[str], str, None]: """ Get the contents of the named entry from the distributions PKG-INFO file """ @@ -109,8 +112,7 @@ def get_meta_entry(dist, name): return meta.get(name) -def get_dist_url(dist): - # type: (pkg_resources.Distribution) -> Optional[str] +def get_dist_url(dist: Distribution) -> Optional[str]: """ Return the 'url' of the distribution (as passed to setup function) """ @@ -119,17 +121,14 @@ def get_dist_url(dist): return url -def get_dist_meta(dist): - # type: (pkg_resources.Distribution) -> Dict[str, Union[str, List[str]]] - contents = None # type: Optional[str] - if dist.has_metadata("PKG-INFO"): - # egg-info - contents = dist.get_metadata("PKG-INFO") - elif dist.has_metadata("METADATA"): - # dist-info - contents = dist.get_metadata("METADATA") - - if contents is not None: - return parse_meta(contents) - else: - return {} +def get_dist_meta(dist: Distribution) -> Dict[str, Union[str, List[str]]]: + metadata = dist.metadata + meta: Dict[str, Union[str, List[str]]] = {} + for key in metadata: + if key == "Description": + meta[key] = trim(metadata[key]) + elif key in MULTIPLE_KEYS: + meta[key] = metadata.get_all(key) + else: + meta[key] = metadata[key] + return meta diff --git a/orangecanvas/utils/tests/test_pkgmeta.py b/orangecanvas/utils/tests/test_pkgmeta.py new file mode 100644 index 00000000..52610150 --- /dev/null +++ b/orangecanvas/utils/tests/test_pkgmeta.py @@ -0,0 +1,41 @@ +from unittest import TestCase + +from orangecanvas.utils.pkgmeta import ( + Distribution, normalize_name, is_develop_egg, get_dist_meta, parse_meta, + trim, +) + + +class TestPkgMeta(TestCase): + def test_normalize_name(self): + self.assertEqual(normalize_name("a-c_4"), "a_c_4") + + def test_is_develop_egg(self): + d = Distribution.from_name("AnyQt") + is_develop_egg(d) + try: + d = Distribution.from_name("orange-canvas-core") + except Exception: + pass + else: + is_develop_egg(d) + + def test_get_dist_meta(self): + d = Distribution.from_name("AnyQt") + meta = get_dist_meta(d) + self.assertEqual(meta["Name"], "AnyQt") + + def test_parse_meta(self): + m = parse_meta(trim(""" + Metadata-Version: 1.0 + Name: AA + Version: 0.1 + Requires-Dist: foo + Requires-Dist: bar + """)) + self.assertEqual(m["Name"], "AA") + self.assertEqual(m["Version"], "0.1") + self.assertEqual(m["Requires-Dist"], ["foo", "bar"]) + + def test_trim(self): + self.assertEqual(trim("A\n a\n b"), "A\na\nb") From aec1a64991897906e62080609768db52053f1540 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 20 Oct 2023 14:43:23 +0200 Subject: [PATCH 02/17] config: Replace use of pkg_resources --- orangecanvas/config.py | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/orangecanvas/config.py b/orangecanvas/config.py index faafab81..950b6592 100644 --- a/orangecanvas/config.py +++ b/orangecanvas/config.py @@ -8,14 +8,14 @@ import logging import warnings import typing +import pkgutil from typing import Dict, Optional, Tuple, List, Union, Iterable, Any import packaging.version -import pkg_resources from AnyQt.QtGui import ( - QPainter, QFont, QFontMetrics, QColor, QPixmap, QIcon + QPainter, QFont, QFontMetrics, QColor, QPixmap, QImage, QIcon ) from AnyQt.QtCore import ( @@ -23,15 +23,15 @@ ) from .gui.utils import windows_set_current_process_app_user_model_id +from .gui.svgiconengine import SvgIconEngine from .utils.settings import Settings, config_slot +from .utils.pkgmeta import EntryPoint, Distribution, entry_points if typing.TYPE_CHECKING: import requests from .scheme import Scheme T = typing.TypeVar("T") -EntryPoint = pkg_resources.EntryPoint -Distribution = pkg_resources.Distribution log = logging.getLogger(__name__) @@ -41,7 +41,7 @@ #: Entry point by which widgets are registered. WIDGETS_ENTRY = "orangecanvas.widgets" -#: Entry point by which add-ons register with pkg_resources. +#: Entry point by which add-ons register with importlib.metadata ADDONS_ENTRY = "orangecanvas.addon" #: Parameters for searching add-on packages in PyPi using xmlrpc api. @@ -218,10 +218,8 @@ def application_icon(): """ Return the main application icon. """ - path = pkg_resources.resource_filename( - __name__, "icons/orange-canvas.svg" - ) - return QIcon(path) + data = pkgutil.get_data(__name__, "icons/orange-canvas.svg") + return QIcon(SvgIconEngine(data)) @staticmethod def splash_screen(): @@ -239,10 +237,10 @@ def splash_screen(): t : Tuple[QPixmap, QRect] A QPixmap and a rect area within it. """ - path = pkg_resources.resource_filename( - __name__, "icons/orange-canvas-core-splash.svg") - pm = QPixmap(path) + contents = pkgutil.get_data(__name__, "icons/orange-canvas-core-splash.svg") + img = QImage.fromData(contents, "svg") + pm = QPixmap.fromImage(img) version = QCoreApplication.applicationVersion() if version: version_parsed = packaging.version.Version(version) @@ -269,18 +267,16 @@ def splash_screen(): return pm, textarea @staticmethod - def widgets_entry_points(): - # type: () -> Iterable[EntryPoint] + def widgets_entry_points() -> Iterable[EntryPoint]: """ Return an iterator over entry points defining the set of 'nodes/widgets' available to the workflow model. """ - return pkg_resources.iter_entry_points(WIDGETS_ENTRY) + return iter(entry_points(group=WIDGETS_ENTRY)) @staticmethod - def addon_entry_points(): - # type: () -> Iterable[EntryPoint] - return pkg_resources.iter_entry_points(ADDONS_ENTRY) + def addon_entry_points() -> Iterable[EntryPoint]: + return iter(entry_points(group=ADDONS_ENTRY)) @staticmethod def addon_pypi_search_spec(): @@ -323,7 +319,7 @@ def core_packages(): @staticmethod def examples_entry_points(): - return pkg_resources.iter_entry_points(EXAMPLE_WORKFLOWS_ENTRY) + return iter(entry_points(group=EXAMPLE_WORKFLOWS_ENTRY)) @staticmethod def widget_discovery(*args, **kwargs): From 14cd2efdc01146288f973bae3ee9d12303e5f633 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 27 Oct 2023 10:31:14 +0200 Subject: [PATCH 03/17] addons: Replace use of pkg_resources --- orangecanvas/application/addons.py | 21 ++--- orangecanvas/application/canvasmain.py | 2 +- orangecanvas/application/tests/test_addons.py | 40 ++++++-- .../application/tests/test_addons_utils.py | 20 ++-- orangecanvas/application/utils/addons.py | 94 ++++++++----------- orangecanvas/utils/pkgmeta.py | 12 ++- 6 files changed, 107 insertions(+), 82 deletions(-) diff --git a/orangecanvas/application/addons.py b/orangecanvas/application/addons.py index 4161bb3d..820e6cb2 100644 --- a/orangecanvas/application/addons.py +++ b/orangecanvas/application/addons.py @@ -9,7 +9,7 @@ from typing import List, Any, Optional, Tuple -import pkg_resources +from packaging.requirements import Requirement from AnyQt.QtWidgets import ( QDialog, QLineEdit, QTreeView, QHeaderView, @@ -52,9 +52,8 @@ from .. import config from ..config import Config +from ..utils.pkgmeta import Distribution -Requirement = pkg_resources.Requirement -Distribution = pkg_resources.Distribution log = logging.getLogger(__name__) @@ -155,7 +154,7 @@ def createRow(item): if isinstance(item, Installed): installed = True ins, dist = item.installable, item.local - name = prettify_name(dist.project_name) + name = prettify_name(dist.name) summary = get_dist_meta(dist).get("Summary", "") version = dist.version item_is_core = item.required @@ -553,14 +552,14 @@ def network_warning(exc): if ep.dist is not None] items = installable_items(packages, installed) core_constraints = { - r.project_name.casefold(): r - for r in (Requirement.parse(r) for r in config.core_packages()) + r.name.casefold(): r + for r in map(Requirement, config.core_packages()) } def constrain(item): # type: (Item) -> Item """Include constraint in Installed when in core_constraint""" if isinstance(item, Installed): - name = item.local.project_name.casefold() + name = item.local.name.casefold() if name in core_constraints: return item._replace( required=True, constraint=core_constraints[name] @@ -686,7 +685,7 @@ def match(item): elif item.installable is not None: return item.installable.name == installable.name else: - return item.local.project_name.lower() == installable.name.lower() + return item.local.name.lower() == installable.name.lower() new = next(filter(match, new_), None) assert new is not None @@ -858,7 +857,7 @@ def __accepted(self): core_required = {} for item in self.items(): if isinstance(item, Installed) and item.required: - core_required[item.local.project_name] = item.local.version + core_required[item.local.name] = item.local.version core_upgrade = set() for step in steps: @@ -867,8 +866,8 @@ def __accepted(self): if inst.name in core_required: # direct upgrade of a core package core_upgrade.add(inst.name) if inst.requirements: # indirect upgrade of a core package as a requirement - for req in pkg_resources.parse_requirements(inst.requirements): - if req.name in core_required and core_required[req.name] not in req: + for req in map(Requirement, inst.requirements): + if req.name in core_required and not req.specifier.contains(core_required[req.name], prereleases=True): core_upgrade.add(req.name) # current doesn't meet requirements if core_upgrade: diff --git a/orangecanvas/application/canvasmain.py b/orangecanvas/application/canvasmain.py index 924c7ca3..4ca6dad0 100644 --- a/orangecanvas/application/canvasmain.py +++ b/orangecanvas/application/canvasmain.py @@ -1354,7 +1354,7 @@ def install_requirements(self, requires: Sequence[str]) -> int: dlg.setStyle(QApplication.style()) dlg.setConfig(config.default) req = addons.Requirement - names = [req.parse(r).project_name for r in requires] + names = [req(r).name for r in requires] normalized_names = {normalize_name(r) for r in names} def set_state(*args): diff --git a/orangecanvas/application/tests/test_addons.py b/orangecanvas/application/tests/test_addons.py index 115bf09f..8fbb7a75 100644 --- a/orangecanvas/application/tests/test_addons.py +++ b/orangecanvas/application/tests/test_addons.py @@ -9,7 +9,6 @@ from AnyQt.QtGui import QDropEvent from AnyQt.QtTest import QTest from AnyQt.QtWidgets import QDialogButtonBox, QMessageBox, QTreeView, QStyle -from pkg_resources import Distribution, EntryPoint from orangecanvas.application import addons from orangecanvas.application.addons import AddonManagerDialog @@ -26,6 +25,7 @@ ) from orangecanvas.gui.test import QAppTestCase from orangecanvas.utils.qinvoke import qinvoke +from orangecanvas.utils.pkgmeta import Distribution, EntryPoint @contextmanager @@ -41,19 +41,46 @@ def addon_archive(pkginfo): os.remove(name) +class FakeDistribution(Distribution): + def locate_file(self, path): + pass + + def read_text(self, filename): + pass + + def __init__(self, name, version): + super().__init__() + self._name = name + self._version = version + + @property + def name(self): + return self._name + + @property + def version(self): + return self._version + + +class FakeEntryPoint(EntryPoint): + def for_(self, dist): + vars(self).update(dist=dist) + return self + + class TestAddonManagerDialog(QAppTestCase): def test_widget(self): items = [ Installed( Installable("foo", "1.1", "", "", "", []), - Distribution(project_name="foo", version="1.0"), + FakeDistribution(name="foo", version="1.0"), ), Available( Installable("q", "1.2", "", "", "", []) ), Installed( None, - Distribution(project_name="a", version="0.0") + FakeDistribution(name="a", version="0.0") ), ] w = AddonManagerDialog() @@ -98,13 +125,14 @@ def check_state_equal(left, right): check_state_equal(index.data(Qt.CheckStateRole), Qt.Unchecked) @patch("orangecanvas.config.default.addon_entry_points", - return_value=[EntryPoint( - "a", "b", dist=Distribution(project_name="foo", version="1.0"))]) + return_value=[ + FakeEntryPoint( + "a", "b", "g").for_(FakeDistribution(name="foo", version="1.0"))]) def test_drop(self, p1): items = [ Installed( Installable("foo", "1.1", "", "", "", []), - Distribution(project_name="foo", version="1.0"), + FakeDistribution(name="foo", version="1.0"), ), ] w = AddonManagerDialog() diff --git a/orangecanvas/application/tests/test_addons_utils.py b/orangecanvas/application/tests/test_addons_utils.py index 95712f23..b3064abc 100644 --- a/orangecanvas/application/tests/test_addons_utils.py +++ b/orangecanvas/application/tests/test_addons_utils.py @@ -3,9 +3,9 @@ import unittest from tempfile import mkdtemp -from pkg_resources import Requirement from requests import Session from requests_cache import CachedSession +from packaging.requirements import Requirement from orangecanvas.application.utils.addons import ( Available, @@ -16,20 +16,26 @@ is_updatable, prettify_name, _session, ) -from orangecanvas.config import Distribution +from orangecanvas.application.tests.test_addons import FakeDistribution class TestUtils(unittest.TestCase): def test_items_1(self): inst = Installable("foo", "1.0", "a foo", "", "", []) - dist = Distribution(project_name="foo", version="1.0") + dist = FakeDistribution(name="foo", version="1.0") item = Available(inst) self.assertFalse(is_updatable(item)) + self.assertEqual(item.name, "foo") + self.assertEqual(item.normalized_name, "foo") item = Installed(None, dist) self.assertFalse(is_updatable(item)) + self.assertEqual(item.name, dist.name) + self.assertEqual(item.normalized_name, dist.name) + item = Installed(inst, dist) self.assertFalse(is_updatable(item)) + self.assertEqual(item.name, inst.name) item = Installed(inst._replace(version="0.9"), dist) self.assertFalse(is_updatable(item)) @@ -38,17 +44,17 @@ def test_items_1(self): self.assertTrue(is_updatable(item)) item = Installed(inst._replace(version="2.0"), dist, - constraint=Requirement.parse("foo<1.99")) + constraint=Requirement("foo<1.99")) self.assertFalse(is_updatable(item)) item = Installed(inst._replace(version="2.0"), dist, - constraint=Requirement.parse("foo<2.99")) + constraint=Requirement("foo<2.99")) self.assertTrue(is_updatable(item)) def test_items_2(self): inst1 = Installable("foo", "1.0", "a foo", "", "", []) inst2 = Installable("bar", "1.0", "a bar", "", "", []) - dist2 = Distribution(project_name="bar", version="0.9") - dist3 = Distribution(project_name="quack", version="1.0") + dist2 = FakeDistribution(name="bar", version="0.9") + dist3 = FakeDistribution(name="quack", version="1.0") items = installable_items([inst1, inst2], [dist2, dist3]) self.assertIn(Available(inst1), items) self.assertIn(Installed(inst2, dist2), items) diff --git a/orangecanvas/application/utils/addons.py b/orangecanvas/application/utils/addons.py index 75cc2860..14e6f80c 100644 --- a/orangecanvas/application/utils/addons.py +++ b/orangecanvas/application/utils/addons.py @@ -11,23 +11,24 @@ from enum import Enum from sqlite3 import OperationalError from types import SimpleNamespace -from typing import AnyStr, Callable, List, NamedTuple, Optional, Tuple, TypeVar, Union +from typing import ( + AnyStr, Callable, List, NamedTuple, Optional, Tuple, TypeVar, Union, Dict, + Any, IO, Iterable +) import requests import requests_cache +from packaging.requirements import Requirement +from packaging.version import Version + from AnyQt.QtCore import QObject, QSettings, QStandardPaths, QTimer, Signal, Slot -from pkg_resources import ( - Requirement, - ResolutionError, - VersionConflict, - WorkingSet, - get_distribution, - parse_version, -) +from orangecanvas import config from orangecanvas.utils import unique -from orangecanvas.utils.pkgmeta import parse_meta +from orangecanvas.utils.pkgmeta import ( + parse_meta, normalize_name, Distribution, get_distribution +) from orangecanvas.utils.shtools import create_process, python_process log = logging.getLogger(__name__) @@ -37,10 +38,6 @@ B = TypeVar("B") -def normalize_name(name): - return re.sub(r"[-_.]+", "-", name).lower() - - def prettify_name(name): dash_split = name.split('-') # Orange3-ImageAnalytics => ImageAnalytics @@ -114,12 +111,12 @@ class Available( installable : Installable """ @property - def project_name(self): + def name(self): return self.installable.name @property def normalized_name(self): - return normalize_name(self.project_name) + return normalize_name(self.name) class Installed( @@ -154,23 +151,22 @@ def __new__(cls, installable, local, required=False, constraint=None): return super().__new__(cls, installable, local, required, constraint) @property - def project_name(self): + def name(self): if self.installable is not None: return self.installable.name else: - return self.local.project_name + return self.local.name @property def normalized_name(self): - return normalize_name(self.project_name) + return normalize_name(self.name) #: An installable item/slot Item = Union[Available, Installed] -def is_updatable(item): - # type: (Item) -> bool +def is_updatable(item: Item) -> bool: if isinstance(item, Available): return False elif item.installable is None: @@ -178,15 +174,16 @@ def is_updatable(item): else: inst, dist = item.installable, item.local try: - v1 = parse_version(dist.version) - v2 = parse_version(inst.version) + v1 = Version(dist.version) + v2 = Version(inst.version) except ValueError: return False if inst.force: return True - if item.constraint is not None and str(v2) not in item.constraint: + if item.constraint is not None \ + and not item.constraint.specifier.contains(v2, prereleases=True): return False else: return v1 < v2 @@ -340,8 +337,10 @@ def query_pypi(names: List[str]) -> List[_QueryResult]: ] -def list_available_versions(config, session=None): - # type: (config.Config, Optional[requests.Session]) -> (List[Installable], List[Exception]) +def list_available_versions( + config: config.Config, + session: Optional[requests.Session] = None +) -> Tuple[List[Installable], List[Exception]]: if session is None: session = _session() @@ -368,7 +367,7 @@ def getname(item): # list installed = [ep.dist for ep in config.addon_entry_points() if ep.dist is not None] - missing = {dist.project_name.casefold() for dist in installed} - \ + missing = {dist.name.casefold() for dist in installed} - \ {name.casefold() for name in defaults_names} distributions = [] @@ -398,27 +397,19 @@ def installable_items(pypipackages, installed=[]): Parameters ---------- - pypipackages : list of Installable - installed : list of pkg_resources.Distribution + pypipackages : List[Installable] + installed : List[Distribution] """ - dists = {dist.project_name: dist for dist in installed} + dists = {dist.name: dist for dist in installed} packages = {pkg.name: pkg for pkg in pypipackages} # For every pypi available distribution not listed by # `installed`, check if it is actually already installed. - ws = WorkingSet() for pkg_name in set(packages.keys()).difference(set(dists.keys())): - try: - d = ws.find(Requirement.parse(pkg_name)) - except ResolutionError: - pass - except ValueError: - # Requirements.parse error ? - pass - else: - if d is not None: - dists[d.project_name] = d + d = get_distribution(pkg_name) + if d is not None: + dists[d.name] = d project_names = unique(itertools.chain(packages.keys(), dists.keys())) @@ -436,19 +427,12 @@ def installable_items(pypipackages, installed=[]): return items -def is_requirement_available( - req: Union[Requirement, str], working_set: Optional[WorkingSet] = None -) -> bool: +def is_requirement_available(req: Union[Requirement, str]) -> bool: if not isinstance(req, Requirement): - req = Requirement.parse(req) + req = Requirement(req) try: - if working_set is None: - d = get_distribution(req) - else: - d = working_set.find(req) - except VersionConflict: - return False - except ResolutionError: + d = get_distribution(req.name) + except Exception: return False else: return d is not None @@ -543,7 +527,7 @@ def _next(self): self.pip.upgrade(pkg.installable) elif command == Uninstall: self.setStatusMessage( - "Uninstalling {}".format(pkg.local.project_name)) + "Uninstalling {}".format(pkg.local.name)) if self.conda: try: self.conda.uninstall(pkg.local) @@ -599,7 +583,7 @@ def upgrade(self, package): run_command(cmd) def uninstall(self, dist): - cmd = ["python", "-m", "pip", "uninstall", "--yes", dist.project_name] + cmd = ["python", "-m", "pip", "uninstall", "--yes", dist.name] run_command(cmd) @@ -644,7 +628,7 @@ def upgrade(self, pkg): def uninstall(self, dist): cmd = [self.conda, "uninstall", "--yes", - self._normalize(dist.project_name)] + self._normalize(dist.name)] return run_command(cmd) def _normalize(self, name): diff --git a/orangecanvas/utils/pkgmeta.py b/orangecanvas/utils/pkgmeta.py index de73098b..aa13f554 100644 --- a/orangecanvas/utils/pkgmeta.py +++ b/orangecanvas/utils/pkgmeta.py @@ -8,13 +8,14 @@ import packaging.version try: - from importlib.metadata import EntryPoint, Distribution, entry_points + from importlib.metadata import EntryPoint, Distribution, entry_points, PackageNotFoundError except ImportError: - from importlib_metadata import EntryPoint, Distribution, entry_points + from importlib_metadata import EntryPoint, Distribution, entry_points, PackageNotFoundError __all__ = [ "Distribution", "EntryPoint", "entry_points", "normalize_name", "trim", "trim_leading_lines", "trim_trailing_lines", "parse_meta", "get_dist_meta", + "get_distribution" ] @@ -132,3 +133,10 @@ def get_dist_meta(dist: Distribution) -> Dict[str, Union[str, List[str]]]: else: meta[key] = metadata[key] return meta + + +def get_distribution(name: str) -> Optional[Distribution]: + try: + return Distribution.from_name(name) + except PackageNotFoundError: + return None From ef4a2f923fe72e8b239755a64188481bb9f84de7 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 27 Oct 2023 13:29:48 +0200 Subject: [PATCH 04/17] examples: Replace use of pkg_resources --- orangecanvas/application/examples.py | 36 ++++++++++++++-------------- setup.py | 1 + 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/orangecanvas/application/examples.py b/orangecanvas/application/examples.py index c17a4d81..0858659d 100644 --- a/orangecanvas/application/examples.py +++ b/orangecanvas/application/examples.py @@ -3,13 +3,18 @@ """ import os import logging +import pathlib import types from typing import List, Optional, IO -import pkg_resources - from orangecanvas import config as _config +from orangecanvas.utils.pkgmeta import Distribution + +try: + from importlib.resources import files as _files +except ImportError: + from importlib_resources import files as _files log = logging.getLogger(__name__) @@ -24,8 +29,8 @@ def is_ows(filename): # type: (str) -> bool return filename.endswith(".ows") - resources = pkg_resources.resource_listdir(package.__name__, ".") - return sorted(filter(is_ows, resources)) + resources = _files(package.__name__).iterdir() + return sorted(filter(is_ows, (r.name for r in resources))) def workflows(config=None): @@ -45,11 +50,7 @@ def workflows(config=None): examples_entry_points = config.examples_entry_points for ep in examples_entry_points(): try: - examples = ep.resolve() - except pkg_resources.DistributionNotFound as ex: - log.warning("Could not load examples from %r (%r)", - ep.dist, ex) - continue + examples = ep.load() except Exception: log.error("Could not load examples from %r", ep.dist, exc_info=True) @@ -75,34 +76,33 @@ def workflows(config=None): class ExampleWorkflow: def __init__(self, resource, package=None, distribution=None): - # type: (str, Optional[types.ModuleType], Optional[pkg_resources.Distribution]) -> None + # type: (str, Optional[types.ModuleType], Optional[Distribution]) -> None self.resource = resource self.package = package self.distribution = distribution - def abspath(self): - # type: () -> str + def abspath(self) -> str: """ Return absolute filename for the workflow if possible else raise an ValueError. """ if self.package is not None: - return pkg_resources.resource_filename(self.package.__name__, - self.resource) + item = _files(self.package) / self.resource + if isinstance(item, pathlib.Path): + return str(item) elif isinstance(self.resource, str): if os.path.isabs(self.resource): return self.resource raise ValueError("cannot resolve resource to an absolute name") - def stream(self): - # type: () -> IO[bytes] + def stream(self) -> IO[bytes]: """ Return the example file as an open stream. """ if self.package is not None: - return pkg_resources.resource_stream(self.package.__name__, - self.resource) + item = _files(self.package) / self.resource + return item.open('rb') elif isinstance(self.resource, str): if os.path.isabs(self.resource) and os.path.exists(self.resource): return open(self.resource, "rb") diff --git a/setup.py b/setup.py index dfb74099..7cf83ec0 100755 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ "dictdiffer", "qasync>=0.10.0", "importlib_metadata; python_version<'3.8'", + "importlib_resources; python_version<'3.9'", "packaging", "numpy", ) From 488f691d1dbd3dda3c764703a62f96e6be8c4979 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 27 Oct 2023 14:58:43 +0200 Subject: [PATCH 05/17] registry: Replace use of pkg_resources Also remove some unused functions. --- orangecanvas/registry/description.py | 2 +- orangecanvas/registry/discovery.py | 61 +++---------------- orangecanvas/registry/tests/test_discovery.py | 6 +- 3 files changed, 14 insertions(+), 55 deletions(-) diff --git a/orangecanvas/registry/description.py b/orangecanvas/registry/description.py index d76150fb..d694a6f2 100644 --- a/orangecanvas/registry/description.py +++ b/orangecanvas/registry/description.py @@ -456,7 +456,7 @@ class CategoryDescription(object): priority : int Priority (order in the GUI). icon : str - An icon filename (a resource name retrievable using `pkg_resources` + An icon filename (a resource name retrievable using `pkgutil.get_data` relative to `qualified_name`). background : str An background color for widgets in this category. diff --git a/orangecanvas/registry/discovery.py b/orangecanvas/registry/discovery.py index b9bc85a8..6cc827db 100644 --- a/orangecanvas/registry/discovery.py +++ b/orangecanvas/registry/discovery.py @@ -10,15 +10,12 @@ import abc import os import sys -import stat import logging import types import pkgutil from collections import namedtuple from typing import Union -import pkg_resources - from .description import ( WidgetDescription, CategoryDescription, WidgetSpecificationError, CategorySpecificationError @@ -27,6 +24,7 @@ from . import VERSION_HEX from . import cache, WidgetRegistry from . import utils +from ..utils.pkgmeta import entry_points log = logging.getLogger(__name__) @@ -119,27 +117,18 @@ def __init__( def run(self, entry_points_iter): """ Run the widget discovery process from an entry point iterator - (yielding :class:`pkg_resources.EntryPoint` instances). + (yielding :class:`importlib.metadata.EntryPoint` instances). As a convenience, if `entry_points_iter` is a string it will be used - to retrieve the iterator using `pkg_resources.iter_entry_points`. + to retrieve the iterator using `importlib.metadata.entry_points`. """ if isinstance(entry_points_iter, str): - entry_points_iter = \ - pkg_resources.iter_entry_points(entry_points_iter) + entry_points_iter = entry_points(group=entry_points_iter) for entry_point in entry_points_iter: try: - point = entry_point.resolve() - except pkg_resources.DistributionNotFound: - log.error("Could not load '%s' (unsatisfied dependencies).", - entry_point, exc_info=True) - continue - except ImportError: - log.error("An ImportError occurred while loading " - "entry point '%s'", entry_point, exc_info=True) - continue + point = entry_point.load() except Exception: log.error("An exception occurred while loading " "entry point '%s'", entry_point, exc_info=True) @@ -221,7 +210,7 @@ def process_category_package(self, category, name=None, distribution=None): cat_desc.name = default_category_name_for_module(category) if distribution is not None: - cat_desc.project_name = distribution.project_name + cat_desc.project_name = distribution.name self.handle_category(cat_desc) @@ -360,7 +349,7 @@ def widget_description(self, module, widget_name=None, desc.category = category_name if distribution is not None: - desc.project_name = distribution.project_name + desc.project_name = distribution.name return desc @@ -380,7 +369,7 @@ def cache_insert(self, module, mtime, description, distribution=None, project_name = project_version = None if distribution is not None: - project_name = distribution.project_name + project_name = distribution.name project_version = distribution.version exc_type = exc_val = None @@ -423,7 +412,7 @@ def cache_has_valid_entry(self, mod_path, distribution=None): return False if distribution is not None: - if entry.project_name != distribution.project_name or \ + if entry.project_name != distribution.name or \ entry.project_version != distribution.version: return False @@ -508,38 +497,6 @@ def widget_descriptions_from_package(package): return desciptions -def module_name_split(name): - """ - Split the module name into package name and module name. - """ - if "." in name: - package_name, module = name.rsplit(".", 1) - else: - package_name, module = "", name - return package_name, module - - -def module_modified_time(module): - """ - Return the `module`s source filename and modified time as a tuple - (source_filename, modified_time). In case the module is from a zipped - package the modified time is that of of the archive. - - """ - module = asmodule(module) - name = module.__name__ - module_filename = module.__file__ - - provider = pkg_resources.get_provider(name) - if provider.loader: - m_time = os.stat(provider.loader.archive)[stat.ST_MTIME] - else: - basename = os.path.basename(module_filename) - path = pkg_resources.resource_filename(name, basename) - m_time = os.stat(path)[stat.ST_MTIME] - return (module_filename, m_time) - - def asmodule(module): """ Return the module references by `module` name. If `module` is diff --git a/orangecanvas/registry/tests/test_discovery.py b/orangecanvas/registry/tests/test_discovery.py index ae589274..06ce1279 100644 --- a/orangecanvas/registry/tests/test_discovery.py +++ b/orangecanvas/registry/tests/test_discovery.py @@ -9,6 +9,7 @@ from ..discovery import WidgetDiscovery, widget_descriptions_from_package from ..description import CategoryDescription, WidgetDescription from ..utils import category_from_package_globals +from ...utils.pkgmeta import get_distribution class TestDiscovery(unittest.TestCase): @@ -40,8 +41,9 @@ def test_handle(self): def test_process_module(self): disc = self.discovery_class() - disc.process_category_package(self.operators.__name__) - disc.process_widget_module(self.constants.one.__name__) + dist = get_distribution("orange-canvas-core") + disc.process_category_package(self.operators.__name__, distribution=dist) + disc.process_widget_module(self.constants.one.__name__, distribution=dist) def test_process_loader(self): disc = self.discovery_class() From e57fcc732599ea52f1ca96c606cca3fd2e035879 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 27 Oct 2023 14:59:06 +0200 Subject: [PATCH 06/17] styles: Replace use of pkg_resources --- orangecanvas/styles/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/orangecanvas/styles/__init__.py b/orangecanvas/styles/__init__.py index a3662937..9101f990 100644 --- a/orangecanvas/styles/__init__.py +++ b/orangecanvas/styles/__init__.py @@ -1,11 +1,11 @@ """ """ import os +import pkgutil import re from functools import wraps from typing import Mapping, Callable, Tuple, List, TypeVar -import pkg_resources from AnyQt.QtCore import Qt from AnyQt.QtGui import QPalette, QColor @@ -182,12 +182,11 @@ def process_qss(content: str, base: str): # no extension stylesheet = os.path.extsep.join([stylesheet, "qss"]) - pkg_name = __package__ resource = stylesheet - - if pkg_resources.resource_exists(pkg_name, resource): - stylesheet_string = \ - pkg_resources.resource_string(pkg_name, resource).decode("utf-8") - base = pkg_resources.resource_filename(pkg_name, "") - return process_qss(stylesheet_string, base) - return stylesheet_string, [] + try: + stylesheet_string = pkgutil.get_data(__package__, resource) + except FileNotFoundError: + return stylesheet_string, [] + else: + return process_qss(stylesheet_string.decode("utf-8"), + os.path.basename(__file__)) From 664dff4b69e371ea1fb4a196767b6cfb66ffc6ed Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 2 Nov 2023 11:27:15 +0100 Subject: [PATCH 07/17] test_main: Adapt test to importlib.metadata.EntryPoint Also increase coverage for config tests --- orangecanvas/application/tests/test_main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/orangecanvas/application/tests/test_main.py b/orangecanvas/application/tests/test_main.py index 927375e8..8f0f0a9d 100644 --- a/orangecanvas/application/tests/test_main.py +++ b/orangecanvas/application/tests/test_main.py @@ -7,13 +7,14 @@ from orangecanvas import config from orangecanvas.application.canvasmain import CanvasMainWindow -from orangecanvas.config import Config, EntryPoint +from orangecanvas.config import Config from orangecanvas.gui.test import QAppTestCase from orangecanvas.main import Main from orangecanvas.registry import WidgetDiscovery from orangecanvas.registry.tests import set_up_modules, tear_down_modules from orangecanvas.scheme import Scheme from orangecanvas.utils.shtools import temp_named_file +from orangecanvas.utils.pkgmeta import EntryPoint class TestMain(unittest.TestCase): @@ -76,13 +77,16 @@ def widget_discovery(self, *args, **kwargs): def widgets_entry_points(self): # type: () -> Iterable[EntryPoint] pkg = "orangecanvas.registry.tests" return ( - EntryPoint.parse(f"add = {pkg}.operators.add"), - EntryPoint.parse(f"sub = {pkg}.operators.sub") + EntryPoint("add", f"{pkg}.operators.add", "w"), + EntryPoint("sub", f"{pkg}.operators.sub", "w") ) def workflow_constructor(self, *args, **kwargs): return Scheme(*args, **kwargs) + def splash_screen(self): + return config.Default.splash_screen() + class TestMainGuiCase(QAppTestCase): def setUp(self): From f7bff82b0fff143b5ee0f2291b7445af305aae59 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 2 Nov 2023 11:32:05 +0100 Subject: [PATCH 08/17] test_splashscreen: Replace use of pkg_resources --- orangecanvas/gui/tests/test_splashscreen.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/orangecanvas/gui/tests/test_splashscreen.py b/orangecanvas/gui/tests/test_splashscreen.py index 0e2729b1..c26e9a1d 100644 --- a/orangecanvas/gui/tests/test_splashscreen.py +++ b/orangecanvas/gui/tests/test_splashscreen.py @@ -1,12 +1,10 @@ """ Test for splashscreen """ - +import pkgutil from datetime import datetime -import pkg_resources - -from AnyQt.QtGui import QPixmap +from AnyQt.QtGui import QPixmap, QImage from AnyQt.QtCore import Qt, QRect, QTimer from ..splashscreen import SplashScreen @@ -17,11 +15,12 @@ class TestSplashScreen(QAppTestCase): def test_splashscreen(self): - splash = pkg_resources.resource_filename( + contents = pkgutil.get_data( config.__package__, "icons/orange-canvas-core-splash.svg" ) + img = QImage.fromData(contents) w = SplashScreen() - w.setPixmap(QPixmap(splash)) + w.setPixmap(QPixmap.fromImage(img)) w.setTextRect(QRect(100, 100, 400, 50)) w.show() From 951637ec5530b883966d3eeda8a7ee4e43ad288d Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 2 Nov 2023 11:38:23 +0100 Subject: [PATCH 09/17] help/manager: Replace use of pkg_resources --- orangecanvas/help/manager.py | 47 +++++++++----------- orangecanvas/help/tests/test_manager.py | 57 +++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 orangecanvas/help/tests/test_manager.py diff --git a/orangecanvas/help/manager.py b/orangecanvas/help/manager.py index 22e36eef..7db9227a 100644 --- a/orangecanvas/help/manager.py +++ b/orangecanvas/help/manager.py @@ -12,17 +12,15 @@ import typing from typing import Dict, Optional, List, Tuple, Union, Callable, Sequence -import pkg_resources from AnyQt.QtCore import QObject, QUrl, QDir -from ..utils.pkgmeta import get_dist_url, is_develop_egg +from ..utils.pkgmeta import get_dist_url, is_develop_egg, get_distribution from . import provider if typing.TYPE_CHECKING: from ..registry import WidgetRegistry, WidgetDescription - Distribution = pkg_resources.Distribution - EntryPoint = pkg_resources.EntryPoint + from ..utils.pkgmeta import Distribution, EntryPoint log = logging.getLogger(__name__) @@ -62,8 +60,8 @@ def get_provider(self, project: str) -> Optional[provider.HelpProvider]: provider = self._providers.get(project, None) if provider is None: try: - dist = pkg_resources.get_distribution(project) - except pkg_resources.ResolutionError: + dist = get_distribution(project) + except ImportError: log.exception("Could not get distribution for '%s'", project) else: try: @@ -147,8 +145,8 @@ def qurl_query_items(url: QUrl) -> List[Tuple[str, str]]: def _replacements_for_dist(dist): # type: (Distribution) -> Dict[str, str] - replacements = {"PROJECT_NAME": dist.project_name, - "PROJECT_NAME_LOWER": dist.project_name.lower(), + replacements = {"PROJECT_NAME": dist.name, + "PROJECT_NAME_LOWER": dist.name.lower(), "PROJECT_VERSION": dist.version, "DATA_DIR": get_path("data")} try: @@ -157,7 +155,7 @@ def _replacements_for_dist(dist): pass if is_develop_egg(dist): - replacements["DEVELOP_ROOT"] = dist.location + replacements["DEVELOP_ROOT"] = str(dist.locate_file(".")) return replacements @@ -172,7 +170,7 @@ def qurl_from_path(urlpath): def create_intersphinx_provider(entry_point): # type: (EntryPoint) -> Optional[provider.IntersphinxHelpProvider] - locations = entry_point.resolve() + locations = entry_point.load() if entry_point.dist is not None: replacements = _replacements_for_dist(entry_point.dist) else: @@ -221,7 +219,7 @@ def create_intersphinx_provider(entry_point): def create_html_provider(entry_point): # type: (EntryPoint) -> Optional[provider.SimpleHelpProvider] - locations = entry_point.resolve() + locations = entry_point.load() if entry_point.dist is not None: replacements = _replacements_for_dist(entry_point.dist) else: @@ -256,7 +254,7 @@ def create_html_provider(entry_point): def create_html_inventory_provider(entry_point): # type: (EntryPoint) -> Optional[provider.HtmlIndexProvider] - locations = entry_point.resolve() + locations = entry_point.load() if entry_point.dist is not None: replacements = _replacements_for_dist(entry_point.dist) else: @@ -304,8 +302,9 @@ def create_html_inventory_provider(entry_point): _providers_cache = {} # type: Dict[str, provider.HelpProvider] -def get_help_provider_for_distribution(dist): - # type: (pkg_resources.Distribution) -> Optional[provider.HelpProvider] +def get_help_provider_for_distribution( + dist: "Distribution" +) -> Optional[provider.HelpProvider]: """ Return a HelpProvider for the distribution. @@ -321,24 +320,20 @@ def get_help_provider_for_distribution(dist): ------- provider: Optional[provider.HelpProvider] """ - if dist.project_name in _providers_cache: - return _providers_cache[dist.project_name] - - eps = dist.get_entry_map() - entry_points = eps.get("orange.canvas.help", {}) + if dist.name in _providers_cache: + return _providers_cache[dist.name] + eps = dist.entry_points + entry_points = eps.select(group="orange.canvas.help") if not entry_points: # alternative name - entry_points = eps.get("orangecanvas.help", {}) + entry_points = eps.select(group="orangecanvas.help") provider = None - for name, entry_point in entry_points.items(): - create = _providers.get(name, None) + for entry_point in entry_points: + create = _providers.get(entry_point.name, None) if create: try: provider = create(entry_point) - except pkg_resources.DistributionNotFound as err: - log.warning("Unsatisfied dependencies (%r)", err) - continue except Exception as ex: log.exception("Exception {}".format(ex)) if provider: @@ -347,5 +342,5 @@ def get_help_provider_for_distribution(dist): break if provider is not None: - _providers_cache[dist.project_name] = provider + _providers_cache[dist.name] = provider return provider diff --git a/orangecanvas/help/tests/test_manager.py b/orangecanvas/help/tests/test_manager.py new file mode 100644 index 00000000..c7301e86 --- /dev/null +++ b/orangecanvas/help/tests/test_manager.py @@ -0,0 +1,57 @@ +import os.path +from unittest.mock import patch +from types import SimpleNamespace as ns + +from orangecanvas.gui.test import QCoreAppTestCase +from orangecanvas.help import HelpManager +from orangecanvas.help.provider import HtmlIndexProvider +from orangecanvas.registry.tests import small_testing_registry +from orangecanvas.utils import pkgmeta + + +class FakeDistribution(pkgmeta.Distribution): + def read_text(self, filename): + pass + + def locate_file(self, path): + return os.path.join(os.path.devnull, path) + + _entry_points = None + _name = None + _version = None + + def __init__(self, name, version, eps): + self._name = name + self._version = version + self._entry_points = eps + + @property + def name(self): + return self._name + + @property + def version(self): + return self._version + + @property + def entry_points(self): + return self._entry_points + + +HELP_PATHS = ( + ("https://example.com/help", ""), +) + + +class TestHelpManager(QCoreAppTestCase): + def test_manager(self): + manager = HelpManager() + reg = small_testing_registry() + manager.set_registry(reg) + ep = pkgmeta.EntryPoint("html-index", f"{__name__}:HELP_PATHS", "-") + eps = ns(select=lambda *_, **__: [ep]) + dist = FakeDistribution("foo", "0.0", eps) + vars(ep).update(dist=dist) + with patch("orangecanvas.help.manager.get_distribution", lambda *_: dist): + provider = manager.get_provider("foobar") + self.assertIsInstance(provider, HtmlIndexProvider) From b605996f03897e148be895625828e90280a5a632 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 2 Nov 2023 11:39:10 +0100 Subject: [PATCH 10/17] test_previewbrowser: Replace use of pkg_resources --- orangecanvas/preview/tests/test_previewbrowser.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/orangecanvas/preview/tests/test_previewbrowser.py b/orangecanvas/preview/tests/test_previewbrowser.py index 816cd7c4..3510e2a7 100644 --- a/orangecanvas/preview/tests/test_previewbrowser.py +++ b/orangecanvas/preview/tests/test_previewbrowser.py @@ -2,19 +2,16 @@ Unittests for PrewiewBrowser widget. """ +import pkgutil + from ...gui import test from ..previewbrowser import PreviewBrowser from ..previewmodel import PreviewItem, PreviewModel from ... import config -import pkg_resources - -svg1 = pkg_resources.resource_string(config.__package__, - "icons/default-category.svg") - -svg2 = pkg_resources.resource_string(config.__package__, - "icons/default-widget.svg") +svg1 = pkgutil.get_data(config.__package__, "icons/default-category.svg") +svg2 = pkgutil.get_data(config.__package__, "icons/default-widget.svg") def construct_test_preview_model(): From 6e7942ed8f39588d7f03deaf0342a448404468b0 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 2 Nov 2023 11:39:48 +0100 Subject: [PATCH 11/17] test_schemeedit: Remove comment reference to pkg_resources --- orangecanvas/document/tests/test_schemeedit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/orangecanvas/document/tests/test_schemeedit.py b/orangecanvas/document/tests/test_schemeedit.py index 3827c19b..4e9da783 100644 --- a/orangecanvas/document/tests/test_schemeedit.py +++ b/orangecanvas/document/tests/test_schemeedit.py @@ -557,7 +557,6 @@ def test_drag_drop(self): @mock.patch.object( PluginDropHandler, "iterEntryPoints", - # "pkg_resources.WorkingSet.iter_entry_points", lambda _: [ EntryPoint( "AA", f"{__name__}:TestDropHandler", "aa" From 0abdc1b9a73cbc1493f777c93b00164105b57499 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 30 Oct 2023 14:19:34 +0100 Subject: [PATCH 12/17] pkgmeta: Use importlib_metadata on Python < 3.10 --- orangecanvas/utils/pkgmeta.py | 7 ++++--- setup.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/orangecanvas/utils/pkgmeta.py b/orangecanvas/utils/pkgmeta.py index aa13f554..e49e1d0f 100644 --- a/orangecanvas/utils/pkgmeta.py +++ b/orangecanvas/utils/pkgmeta.py @@ -7,10 +7,11 @@ import packaging.version -try: - from importlib.metadata import EntryPoint, Distribution, entry_points, PackageNotFoundError -except ImportError: +if sys.version_info < (3, 10): from importlib_metadata import EntryPoint, Distribution, entry_points, PackageNotFoundError +else: + from importlib.metadata import EntryPoint, Distribution, entry_points, PackageNotFoundError + __all__ = [ "Distribution", "EntryPoint", "entry_points", "normalize_name", "trim", diff --git a/setup.py b/setup.py index 7cf83ec0..d678e0cd 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ "pip>=18.0", "dictdiffer", "qasync>=0.10.0", - "importlib_metadata; python_version<'3.8'", + "importlib_metadata; python_version<'3.10'", "importlib_resources; python_version<'3.9'", "packaging", "numpy", From d96af17725afc10ff7c3d17c3afbc4855704a04e Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 30 Oct 2023 14:20:10 +0100 Subject: [PATCH 13/17] interactions: Change imports of entry_points --- orangecanvas/document/interactions.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/orangecanvas/document/interactions.py b/orangecanvas/document/interactions.py index ea25397a..74f25291 100644 --- a/orangecanvas/document/interactions.py +++ b/orangecanvas/document/interactions.py @@ -46,6 +46,7 @@ from . import commands from .editlinksdialog import EditLinksDialog from ..utils import unique +from ..utils.pkgmeta import EntryPoint, entry_points if typing.TYPE_CHECKING: from .schemeedit import SchemeEditWidget @@ -54,12 +55,6 @@ #: Output/Input pair of a link OIPair = Tuple[OutputSignal, InputSignal] -try: - from importlib.metadata import EntryPoint, entry_points -except ImportError: - from importlib_metadata import EntryPoint, entry_points - - log = logging.getLogger(__name__) @@ -2021,7 +2016,7 @@ def iterEntryPoints(self) -> Iterable['EntryPoint']: """ Return an iterator over all entry points. """ - eps = entry_points().get(self.__group, []) + eps = entry_points(group=self.__group) # Can have duplicated entries here if a distribution is *found* via # different `sys.meta_path` handlers and/or on different `sys.path` # entries. From 8d5dd7e4a632b6fd796e4ccf8fc72cf507b66386 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 30 Oct 2023 14:20:35 +0100 Subject: [PATCH 14/17] docs: Replace pkg_resources --- docs/source/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index ceda11b2..cf26be9c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,8 +16,8 @@ import os import shlex -import pkg_resources -dist = pkg_resources.get_distribution("orange-canvas-core") +import importlib_metadata +dist = importlib_metadata.distribution("orange-canvas-core") # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the From ae8326fd8a4979d7365ed4eb96bd2fb96b69cd33 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 2 Nov 2023 13:23:42 +0100 Subject: [PATCH 15/17] setup.py: Remove setuptools as a runtime dependency --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index d678e0cd..94af8f6b 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,6 @@ } INSTALL_REQUIRES = ( - "setuptools", "AnyQt>=0.2.0", "docutils", "commonmark>=0.8.1", From 28d85df8d2b0b59e49f522cf196867681cf01885 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 17 Nov 2023 15:11:03 +0100 Subject: [PATCH 16/17] discovery: Add/fix some tests --- orangecanvas/registry/tests/test_discovery.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/orangecanvas/registry/tests/test_discovery.py b/orangecanvas/registry/tests/test_discovery.py index 06ce1279..12639a21 100644 --- a/orangecanvas/registry/tests/test_discovery.py +++ b/orangecanvas/registry/tests/test_discovery.py @@ -3,8 +3,10 @@ """ import logging +import types import unittest +from unittest.mock import patch from ..discovery import WidgetDiscovery, widget_descriptions_from_package from ..description import CategoryDescription, WidgetDescription @@ -67,12 +69,24 @@ def test_process_iter(self): cat_desc = category_from_package_globals( self.operators.__name__, ) - # TODO: Fix (the widget_description_package does not iterate - # over faked package (no valid operator.__path__) - wid_desc = widget_descriptions_from_package( - self.operators.__name__, - ) - disc.process_iter([cat_desc] + wid_desc) + modules = [ + (None, self.operators.add.__name__, False) + ] + with patch("pkgutil.iter_modules", lambda *_, **__: modules): + wid_desc = widget_descriptions_from_package( + self.operators.__name__, + ) + disc.process_iter([cat_desc] + wid_desc) + + def test_process_category_package(self): + disc = self.discovery_class() + dist = get_distribution("orange-canvas-core") + modules = [ + (None, self.operators.add.__name__, False) + ] + self.operators.__path__ = ["aaa"] + with patch("pkgutil.iter_modules", lambda *_, **__: modules): + disc.process_category_package(self.operators, distribution=dist) def test_run(self): disc = self.discovery_class() From 801837701371e504e40a7ca8d4816659b5bcd3d9 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 24 Nov 2023 12:08:40 +0100 Subject: [PATCH 17/17] Bump version to 0.2.0a1.dev0 Because we break api in discovery with the changing the type of pkg_resources.Distribution with importlib.metadata.Distribution --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 94af8f6b..3a0aaebb 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages NAME = "orange-canvas-core" -VERSION = "0.1.36.dev0" +VERSION = "0.2.0a1.dev0" DESCRIPTION = "Core component of Orange Canvas" with open("README.rst", "rt", encoding="utf-8") as f: