Skip to content

Commit

Permalink
Merge pull request #23 from python-qt-tools/consider_already_imported
Browse files Browse the repository at this point in the history
  • Loading branch information
altendky authored Jul 6, 2021
2 parents 2dfbe48 + 02cc27a commit a9f0da7
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 3 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
qts [unreleased]
================

Features
--------

- If a wrapper has already been imported then :func:`qts.autoset_wrapper` uses it.
(`#23 <https://github.com/python-qt-tools/qts/pull/23>`__)


qts 0.2
=======

Expand Down
1 change: 1 addition & 0 deletions docs/source/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ Exceptions
.. autoclass:: qts.InvalidWrapperError
.. autoclass:: qts.MultipleWrappersAvailableError
.. autoclass:: qts.NoWrapperAvailableError
.. autoclass:: qts.OtherWrapperAlreadyImportedError
.. autoclass:: qts.WrapperAlreadySelectedError
3 changes: 3 additions & 0 deletions docs/source/qts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Selecting a wrapper
A Qt wrapper must be chosen before leveraging the qts compatibility features.
qts is notified of the choice by a call to :func:`qts.set_wrapper`.
:func:`qts.autoset_wrapper` automatically chooses and sets an available wrapper.
In any case, qts checks for wrappers that have already been imported.
The setting of a wrapper will fail if only unsupported wrappers are already imported.
The setting also fails if a supported wrapper other than the one requested is already imported.

.. autofunction:: qts.set_wrapper
.. autofunction:: qts.autoset_wrapper
Expand Down
3 changes: 3 additions & 0 deletions src/qts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
autoset_wrapper,
available_wrapper,
available_wrappers,
check_already_imported_wrappers,
pyqt_5_wrapper,
pyqt_6_wrapper,
pyside_5_wrapper,
Expand All @@ -23,8 +24,10 @@
InvalidWrapperError,
MultipleWrappersAvailableError,
NoWrapperAvailableError,
OtherWrapperAlreadyImportedError,
QtsError,
WrapperAlreadySelectedError,
UnsupportedWrappersError,
)
from qts._version import get_versions

Expand Down
74 changes: 71 additions & 3 deletions src/qts/_core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import importlib.util
import os
import sys
import typing

import attr
Expand Down Expand Up @@ -57,6 +58,8 @@ def set_wrapper(wrapper: Wrapper) -> None:
:raises qts.WrapperAlreadySelectedError: When called and a wrapper has already
been set.
:raises qts.InvalidWrapperError: When called with an invalid wrapper.
:raises qts.OtherWrapperAlreadyImportedError: When another supported wrapper has
already been imported.
"""

# This could accept the new wrapper if it matches the existing selection, but this
Expand All @@ -73,6 +76,12 @@ def set_wrapper(wrapper: Wrapper) -> None:
if wrapper not in supported_wrappers:
raise qts.InvalidWrapperError(wrapper=wrapper)

already_imported = check_already_imported_wrappers()
if len(already_imported) > 0 and wrapper not in already_imported:
raise qts.OtherWrapperAlreadyImportedError(
requested=wrapper, already_imported=already_imported
)

qts.wrapper = wrapper

qts.is_pyqt_5_wrapper = wrapper == pyqt_5_wrapper
Expand All @@ -81,10 +90,54 @@ def set_wrapper(wrapper: Wrapper) -> None:
qts.is_pyside_6_wrapper = wrapper == pyside_6_wrapper


def already_imported_wrapper_names() -> typing.List[str]:
return [
module
for module in sys.modules
if "." not in module
if any(module.startswith(name) for name in ["PyQt", "PySide"])
]


def check_already_imported_wrappers(
wrappers: typing.Optional[typing.Iterable[Wrapper]] = None,
) -> typing.List[Wrapper]:
"""Checks for wrappers that have already been imported and returns any that are
supported. If only unsupported wrappers have been imported then an exception is
raised.
:param wrappers: An iterable of :class:`qts.Wrapper` to use as the supported list.
If unspecified or :object:`None` then :attr:`qts.supported_wrappers` is used.
:returns: A list of the supported wrappers that have already been imported.
:raises qts.UnsupportedWrappersError: When only unsupported wrappers have been
imported.
"""
if wrappers is None:
wrappers = supported_wrappers

already_imported_names = already_imported_wrapper_names()

if len(already_imported_names) == 0:
return []

supported = {wrapper.module_name for wrapper in wrappers}
supported_already_imported = supported.intersection(already_imported_names)

if len(supported_already_imported) == 0:
raise qts.UnsupportedWrappersError(module_names=already_imported_names)

return [
wrapper_by_name(name=module_name) for module_name in supported_already_imported
]


def autoset_wrapper() -> None:
"""Automatically choose and set the wrapper used to back the Qt modules accessed
through qts. If the environment variable ``QTS_WRAPPER`` is set to a name of a
supported wrapper then that wrapper will be used. The lookup is case insensitive.
If a supported wrapper has already been imported then it will be used.
:raises qts.InvalidWrapperError: When an unsupported wrapper name is specified in
the ``QTS_WRAPPER`` environment variable.
Expand All @@ -100,25 +153,40 @@ def autoset_wrapper() -> None:
set_wrapper(wrapper=environment_wrapper)
return

set_wrapper(wrapper=an_available_wrapper())
already_imported = check_already_imported_wrappers()
if len(already_imported) > 0:
available = an_available_wrapper(wrappers=already_imported)
else:
available = an_available_wrapper()

set_wrapper(wrapper=available)


def available_wrappers(
wrappers: typing.Optional[typing.Iterable[Wrapper]] = None,
) -> typing.Sequence[Wrapper]:
"""Get a sequence of the wrappers that are available for use. If ``wrappers`` is
passed, only wrappers that are both available and in the passed iterable will be
returned.
returned. Availability is checked both by installation metadata and any wrappers
that have already been imported.
:returns: The wrappers that are installed and available for use.
"""

if wrappers is None:
wrappers = supported_wrappers

already_imported_names = already_imported_wrapper_names()

available = [
wrapper for wrapper in wrappers if importlib.util.find_spec(wrapper.module_name)
wrapper
for wrapper in wrappers
if (
importlib.util.find_spec(wrapper.module_name)
or wrapper.name in already_imported_names
)
]

return available


Expand Down
27 changes: 27 additions & 0 deletions src/qts/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,40 @@ def __init__(self, wrappers: typing.Iterable[qts.Wrapper]):
)


class OtherWrapperAlreadyImportedError(QtsError):
"""Raised when wrappers have already been imported but a call is made to set
another.
"""

def __init__(
self,
requested: qts.Wrapper,
already_imported: typing.Collection[qts.Wrapper],
):
already_imported_string = ", ".join(module.name for module in already_imported)
super().__init__(
f"Requested {requested.name} but only others are already imported:"
f" {already_imported_string}"
)


def name_or_repr(wrapper: qts.Wrapper) -> str:
try:
return wrapper.name
except AttributeError:
return repr(wrapper)


class UnsupportedWrappersError(QtsError):
"""Raised when autosetting a wrapper but unsupported wrappers have already been
imported.
"""

def __init__(self, module_names: typing.Collection[str]) -> None:
s = ", ".join(sorted(module_names))
super().__init__(f"Unsupported wrappers have already been imported: f{s}")


class WrapperAlreadySelectedError(QtsError):
"""Raised when attempting to set a wrapper but one has already been selected."""

Expand Down
152 changes: 152 additions & 0 deletions src/qts/_tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,155 @@ def test():
pytester.makepyfile(content)
run_result = pytester.runpytest_subprocess()
run_result.assert_outcomes(passed=1)


def test_check_already_imported_wrappers_finds_none(pytester: pytest.Pytester) -> None:
content = f"""
import sys
import qts
def test():
already_imported = qts.check_already_imported_wrappers()
assert already_imported == []
"""
pytester.makepyfile(content)
run_result = pytester.runpytest_subprocess()
run_result.assert_outcomes(passed=1)


@pytest.mark.parametrize(argnames=["module_name"], argvalues=[["PyQt4"], ["PySide"]])
def test_check_already_imported_wrappers_raises_for_only_unsupported(
module_name: str,
pytester: pytest.Pytester,
) -> None:
content = f"""
import sys
import pytest
import qts
sys.modules[{module_name!r}] = None
def test():
with pytest.raises(qts.UnsupportedWrappersError):
qts.check_already_imported_wrappers()
"""
pytester.makepyfile(content)
run_result = pytester.runpytest_subprocess()
run_result.assert_outcomes(passed=1)


@pytest.mark.parametrize(
argnames=["module_name"],
argvalues=[[module.name] for module in qts.supported_wrappers],
)
@pytest.mark.parametrize(
argnames=["unsupported_module_names"],
argvalues=[[[]], [["PyQt4"]], [["PySide"]], [["PyQt4", "PySide"]]],
)
def test_check_already_imported_wrappers_returns(
module_name: str,
unsupported_module_names: str,
pytester: pytest.Pytester,
) -> None:
content = f"""
import sys
import pytest
import qts
sys.modules[{module_name!r}] = None
for module_name in {unsupported_module_names}:
sys.modules[module_name] = None
def test():
found = qts.check_already_imported_wrappers()
found_names = [module.name for module in found]
assert found_names == [{module_name!r}]
"""
pytester.makepyfile(content)
run_result = pytester.runpytest_subprocess()
run_result.assert_outcomes(passed=1)


def test_check_already_imported_wrappers_returns_select_wrappers(
pytester: pytest.Pytester,
) -> None:
content = f"""
import sys
import pytest
import qts
sys.modules["PyQt5"] = None
sys.modules["PyQt6"] = None
def test():
wrappers = [qts.pyqt_6_wrapper, qts.pyside_6_wrapper]
found = qts.check_already_imported_wrappers(wrappers=wrappers)
assert found == [qts.pyqt_6_wrapper]
"""
pytester.makepyfile(content)
run_result = pytester.runpytest_subprocess()
run_result.assert_outcomes(passed=1)


@pytest.mark.parametrize(
argnames=["module", "another_module"],
argvalues=[
[module, next(iter(set(qts.supported_wrappers) - {module}))]
for module in qts.supported_wrappers
],
)
def test_set_wrapper_raises_if_another_is_already_imported(
module: qts.Wrapper,
another_module: qts.Wrapper,
pytester: pytest.Pytester,
) -> None:
content = f"""
import sys
import pytest
import qts
sys.modules[{another_module.name!r}] = None
def test():
with pytest.raises(qts.OtherWrapperAlreadyImportedError):
qts.set_wrapper(qts.wrapper_by_name(name={module.name!r}))
"""
pytester.makepyfile(content)
run_result = pytester.runpytest_subprocess()
run_result.assert_outcomes(passed=1)


@pytest.mark.parametrize(
argnames=["module"],
argvalues=[[module] for module in qts.supported_wrappers],
)
def test_autoset_wrapper_uses_already_imported(
module: qts.Wrapper,
pytester: pytest.Pytester,
) -> None:
content = f"""
import sys
import pytest
import qts
sys.modules[{module.name!r}] = None
def test():
qts.autoset_wrapper()
assert qts.wrapper == qts.wrapper_by_name(name={module.name!r})
"""
pytester.makepyfile(content)
run_result = pytester.runpytest_subprocess()
run_result.assert_outcomes(passed=1)

0 comments on commit a9f0da7

Please sign in to comment.