Skip to content

Commit

Permalink
Merge e1e40df into 9e1eaf2
Browse files Browse the repository at this point in the history
  • Loading branch information
tych0 committed Apr 1, 2024
2 parents 9e1eaf2 + e1e40df commit b3f0812
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 37 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
# - /setup.cfg:[mypy]
# If adding new python versions, consider also updating
# python version in .readthedocs.yaml
python-version: [pypy-3.10, 3.9, '3.10', '3.11', '3.12']
python-version: [pypy-3.10, '3.10', '3.11', '3.12']
backend: ['x11', 'wayland']
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -44,8 +44,6 @@ jobs:
env:
BACKEND: ${{ matrix.backend }}
- name: Upload coverage data to coveralls.io
# For speed purposes, we don't do coverage reporting on pypy 3.9
if: ${{ matrix.python-version != 'pypy-3.9' }}
run: |
pip -q install coveralls >=3.3.0
coveralls --service=github
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ jobs:
container: quay.io/pypa/manylinux_2_28_x86_64
strategy:
matrix:
python-version: ["cp3.9", "cp3.10", "cp3.11", "pp3.9"]
python-version: ["cp3.10", "cp3.11", "pp3.9"]
steps:
- name: Install dependencies
run: |
Expand Down Expand Up @@ -176,9 +176,9 @@ jobs:
needs: build-wheel
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.10", "3.11"]
include:
- python-version: "pypy-3.9"
- python-version: "pypy-3.10"
manual-version: "pp3.9"
steps:
- name: Download wheels
Expand Down Expand Up @@ -231,7 +231,7 @@ jobs:
needs: [test-wheel, build-source]
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11"]
python-version: ["3.10", "3.11"]
include:
- python-version: "pypy-3.9"
manual-version: "pp3.9"
Expand Down
116 changes: 107 additions & 9 deletions libqtile/command/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,40 @@
from __future__ import annotations

import traceback
import types
import typing
from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Literal, get_args, get_origin

from libqtile import ipc
from libqtile.command.base import CommandError, CommandException, CommandObject, SelectError
from libqtile.command.graph import CommandGraphCall, CommandGraphNode
from libqtile.log_utils import logger
from libqtile.utils import ColorsType, ColorType # noqa: F401

if TYPE_CHECKING:
from typing import Any

from libqtile.command.graph import SelectorType

SUCCESS = 0
ERROR = 1
EXCEPTION = 2


# these two mask their aliases from elsewhere in the tree (i.e.
# libqtile.extension.base._Extension, and libqtile.layout.base.Layout
#
# since neither of these have constructors from a single string, we can't
# really lift them to a type. probably nobody actually passes us layouts here
# so it doesn't matter, but in the event that it does, we can probably fix it
# up with some eval() hackery whackery.
class _Extension:
pass


class Layout:
pass


def format_selectors(selectors: list[SelectorType]) -> str:
"""Build the path to the selected command graph node"""
path_elements = []
Expand Down Expand Up @@ -308,13 +324,95 @@ def call(self, data: tuple[list[SelectorType], str, tuple, dict]) -> tuple[int,
return ERROR, "No such command"

logger.debug("Command: %s(%s, %s)", name, args, kwargs)
try:
# Check if method is bound
if hasattr(cmd, "__self__"):
return SUCCESS, cmd(*args, **kwargs)

def lift_arg(typ, arg):
# for stuff like int | None, allow either
if get_origin(typ) in [types.UnionType, typing.Union]:
for t in get_args(typ):
if t == types.NoneType:
# special case None? I don't know what this looks like
# coming over IPC
if arg == "":
return None
if arg is None:
return None
continue

try:
return lift_arg(t, arg)
except TypeError:
pass
# uh oh, we couldn't lift it to anything
raise TypeError(f"{arg} is not a {typ}")

# for literals, check that it is one of the valid strings
if get_origin(typ) is Literal:
if arg not in get_args(typ):
raise TypeError(f"{arg} is not one of {get_origin(typ)}")
return arg

if typ is bool:
# >>> bool("False") is True
# True
# ... but we want it to be false :)
if arg == "True" or arg is True:
return True
if arg == "False" or arg is False:
return False
raise TypeError(f"{arg} is not a bool")

if typ is Any:
# can't do any lifting if we don't know the type
return arg

if typ in [_Extension, Layout]:
# these are "complex" objects that can't be created with a
# single string argument. we generally don't expect people to
# be passing these over the command line, so let's ignore then.
return arg

if get_origin(typ) in [list, dict]:
# again, we do not want to be in the business of parsing
# lists/dicts of types out of strings; just pass on whatever we
# got
return arg

return typ(arg)

converted_args = []
converted_kwargs = dict()

params = typing.get_type_hints(cmd, globalns=globals())

non_return_annotated_args = filter(lambda k: k != "return", params.keys())
for param, arg in zip(non_return_annotated_args, args):
converted_args.append(lift_arg(params[param], arg))

# if not all args were annotated, we need to keep them anyway. note
# that mixing some annotated and not annotated args will not work well:
# we will reorder args here and cause problems. this is solveable but
# somewhat ugly, and we can avoid it by always annotating all
# parameters.
#
# if we really want to fix this, we
# inspect.signature(foo).parameters.keys() gives us the ordered
# parameters to reason about.
if len(converted_args) < len(args):
converted_args.extend(args[len(converted_args) :])

# Check if method is bound
if not hasattr(cmd, "__self__"):
converted_args.insert(0, obj)

for k, v in kwargs.items():
# if this kwarg has a type annotation, use it
if k in params:
converted_kwargs[k] = lift_arg(params[k], v)
else:
# If not, pass object as first argument
return SUCCESS, cmd(obj, *args, **kwargs)
converted_kwargs[k] = v

try:
return SUCCESS, cmd(*converted_args, **converted_kwargs)
except CommandError as err:
return ERROR, err.args[0]
except Exception:
Expand Down
4 changes: 2 additions & 2 deletions libqtile/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,7 @@ def list_widgets(self) -> list[str]:
return list(self.widgets_map.keys())

@expose_command()
def to_layout_index(self, index: str, name: str | None = None) -> None:
def to_layout_index(self, index: int, name: str | None = None) -> None:
"""
Switch to the layout with the given index in self.layouts.
Expand Down Expand Up @@ -1224,7 +1224,7 @@ def validate_config(self) -> None:

@expose_command()
def spawn(
self, cmd: str | list[str], shell: bool = False, env: dict[str, str] = dict()
self, cmd: list[str] | str, shell: bool = False, env: dict[str, str] = dict()
) -> int:
"""
Spawn a new process.
Expand Down
2 changes: 1 addition & 1 deletion libqtile/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def layout(self, layout):
return
logger.error("No such layout: %s", layout)

def use_layout(self, index):
def use_layout(self, index: int):
assert -len(self.layouts) <= index < len(self.layouts), "layout index out of bounds"
self.layout.hide()
self.current_layout = index % len(self.layouts)
Expand Down
17 changes: 8 additions & 9 deletions libqtile/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,7 @@
from pathlib import Path
from random import randint
from shutil import which
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, Callable, Coroutine, TypeVar, Union

ColorType = Union[str, tuple[int, int, int], tuple[int, int, int, float]]
ColorsType = Union[ColorType, list[ColorType]]

T = TypeVar("T")
from typing import TYPE_CHECKING, Union

try:
from dbus_next import AuthError, Message, Variant
Expand All @@ -52,6 +44,13 @@

from libqtile.log_utils import logger

ColorType = Union[str, tuple[int, int, int], tuple[int, int, int, float]]
ColorsType = Union[ColorType, list[ColorType]]
if TYPE_CHECKING:
from typing import Any, Callable, Coroutine, TypeVar

T = TypeVar("T")

dbus_bus_connections = set()

# Create a list to collect references to tasks so they're not garbage collected
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ classifiers = [
"Operating System :: POSIX :: BSD :: FreeBSD",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3 :: Only",
"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 :: Desktop Environment :: Window Managers",
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[options]
packages = find_namespace:
python_requires >= 3.9
python_requires >= 3.10
setup_requires =
cffi >= 1.1.0
cairocffi[xcb] >= 1.6.0
Expand Down
17 changes: 17 additions & 0 deletions test/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from libqtile.command.base import CommandObject, expose_command
from libqtile.command.interface import CommandError
from libqtile.confreader import Config
from libqtile.ipc import IPCError
from libqtile.lazy import lazy
from test.conftest import dualmonitor

Expand Down Expand Up @@ -88,6 +89,22 @@ def test_layout_filter(manager):
assert manager.c.get_groups()["a"]["focus"] == "two"


@call_config
def test_param_hoisting(manager):
manager.test_window("two")
# 'zomg' is not a valid warp command
with pytest.raises(IPCError):
manager.c.window.focus(warp="zomg")

manager.c.window.focus(warp="False")

# 'zomg' is not a valid bar position
with pytest.raises(IPCError):
manager.c.hide_show_bar(position="zomg")

manager.c.hide_show_bar(position="top")


class FakeCommandObject(CommandObject):
@staticmethod
@expose_command()
Expand Down
4 changes: 2 additions & 2 deletions test/widgets/test_tasklist.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from libqtile.widget.tasklist import TaskList


class TestTaskList(TaskList):
class TaskListTestWidget(TaskList):
def __init__(self, *args, **kwargs):
TaskList.__init__(self, *args, **kwargs)
self._text = ""
Expand Down Expand Up @@ -64,7 +64,7 @@ class TasklistConfig(Config):
floating_layout = libqtile.resources.default_config.floating_layout
keys = []
mouse = []
screens = [Screen(top=bar.Bar([TestTaskList(name="tasklist", **config)], 28))]
screens = [Screen(top=bar.Bar([TaskListTestWidget(name="tasklist", **config)], 28))]

manager_nospawn.start(TasklistConfig)
yield manager_nospawn
Expand Down
10 changes: 5 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ commands =
!x11: {toxinidir}/scripts/ffibuild

# These are the environments that should be triggered by Github Actions
[testenv:py{py3,39,310,311}-{wayland,x11}]
[testenv:py{py3,39,310,311,312}-{wayland,x11}]
# This is required in order to get UTF-8 output inside of the subprocesses
# that our tests use.
setenv = LC_CTYPE = en_US.UTF-8
Expand All @@ -54,8 +54,8 @@ commands =
# pypy3 is very slow when running coverage reports so we skip it
pypy3-x11: python3 -m pytest --backend=x11 {posargs}
pypy3-wayland: python3 -m pytest --backend=wayland {posargs}
py3{9,10,11}-x11: coverage run -m pytest --backend=x11 {posargs}
py3{9,10,11}-wayland: coverage run -m pytest --backend=wayland {posargs}
py3{10,11,12}-x11: coverage run -m pytest --backend=x11 {posargs}
py3{10,11,12}-wayland: coverage run -m pytest --backend=wayland {posargs}

# Coverage is only run via GithubActions
# Coverage runs tests in parallel so we need to combine results into a single file
Expand Down Expand Up @@ -122,9 +122,9 @@ commands =
[gh-actions]
python =
pypy-3.10: pypy3
3.9: py39
3.10: py310
3.11: py311, packaging
3.11: py311
3.12: py312, packaging

[gh-actions:env]
BACKEND =
Expand Down

0 comments on commit b3f0812

Please sign in to comment.