PLC0415: fix

This commit is contained in:
Jörg Thalheim
2025-08-26 14:10:30 +02:00
parent 9c9adc6e16
commit b2a54f5b0d
36 changed files with 146 additions and 219 deletions

View File

@@ -43,7 +43,7 @@ class ApiBridge(ABC):
def process_request(self, request: BackendRequest) -> None:
"""Process an API request through the middleware chain."""
from .middleware import MiddlewareContext
from .middleware import MiddlewareContext # noqa: PLC0415
with ExitStack() as stack:
context = MiddlewareContext(

View File

@@ -17,6 +17,7 @@ from clan_app.api.middleware import (
LoggingMiddleware,
MethodExecutionMiddleware,
)
from clan_app.deps.http.http_server import HttpApiServer
from clan_app.deps.webview.webview import Size, SizeHint, Webview
log = logging.getLogger(__name__)
@@ -65,8 +66,6 @@ def app_run(app_opts: ClanAppOptions) -> int:
# Start HTTP API server if requested
http_server = None
if app_opts.http_api:
from clan_app.deps.http.http_server import HttpApiServer
openapi_file = os.getenv("OPENAPI_FILE", None)
swagger_dist = os.getenv("SWAGGER_UI_DIST", None)

View File

@@ -12,12 +12,11 @@ from clan_lib.api import MethodRegistry, message_queue
from clan_lib.api.tasks import WebThread
from ._webview_ffi import _encode_c_string, _webview_lib
from .webview_bridge import WebviewBridge
if TYPE_CHECKING:
from clan_app.api.middleware import Middleware
from .webview_bridge import WebviewBridge
log = logging.getLogger(__name__)
@@ -49,7 +48,7 @@ class Webview:
shared_threads: dict[str, WebThread] | None = None
# initialized later
_bridge: "WebviewBridge | None" = None
_bridge: WebviewBridge | None = None
_handle: Any | None = None
_callbacks: dict[str, Callable[..., Any]] = field(default_factory=dict)
_middleware: list["Middleware"] = field(default_factory=list)
@@ -132,10 +131,8 @@ class Webview:
self._middleware.append(middleware)
def create_bridge(self) -> "WebviewBridge":
def create_bridge(self) -> WebviewBridge:
"""Create and initialize the WebviewBridge with current middleware."""
from .webview_bridge import WebviewBridge
# Use shared_threads if provided, otherwise let WebviewBridge use its default
if self.shared_threads is not None:
bridge = WebviewBridge(

View File

@@ -8,8 +8,6 @@ from clan_lib.api.tasks import WebThread
from clan_app.api.api_bridge import ApiBridge, BackendRequest, BackendResponse
from .webview import FuncStatus
if TYPE_CHECKING:
from .webview import Webview
@@ -32,6 +30,9 @@ class WebviewBridge(ApiBridge):
)
log.debug(f"Sending response: {serialized}")
# Import FuncStatus locally to avoid circular import
from .webview import FuncStatus # noqa: PLC0415
self.webview.return_(response._op_key, FuncStatus.SUCCESS, serialized) # noqa: SLF001
def handle_webview_call(

View File

@@ -8,6 +8,7 @@ from clan_cli.tests import fixtures_flakes
from clan_cli.tests.age_keys import SopsSetup, assert_secrets_file_recipients
from clan_cli.tests.helpers import cli
from clan_cli.tests.stdout import CaptureOutput
from clan_lib.errors import ClanError
from clan_lib.flake import Flake
from clan_lib.persist.inventory_store import InventoryStore
@@ -93,8 +94,6 @@ def test_machines_update_nonexistent_machine(
test_flake_with_core: fixtures_flakes.FlakeForTest,
) -> None:
"""Test that update command gives helpful error messages for non-existent machines."""
from clan_lib.errors import ClanError
with pytest.raises(ClanError) as exc_info:
cli.run(
[
@@ -118,8 +117,6 @@ def test_machines_update_typo_in_machine_name(
test_flake_with_core: fixtures_flakes.FlakeForTest,
) -> None:
"""Test that update command suggests similar machine names for typos."""
from clan_lib.errors import ClanError
with pytest.raises(ClanError) as exc_info:
cli.run(
[

View File

@@ -1,6 +1,7 @@
import json
import logging
import shutil
import subprocess
from pathlib import Path
import pytest
@@ -11,6 +12,7 @@ from clan_cli.vars.check import check_vars
from clan_cli.vars.generator import (
Generator,
GeneratorKey,
dependencies_as_dir,
)
from clan_cli.vars.get import get_machine_var
from clan_cli.vars.graph import all_missing_closure, requested_closure
@@ -21,7 +23,7 @@ from clan_cli.vars.set import set_var
from clan_lib.errors import ClanError
from clan_lib.flake import Flake
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_eval, run
from clan_lib.nix import nix_config, nix_eval, run
from clan_lib.vars.generate import (
get_generators,
run_generators,
@@ -29,8 +31,6 @@ from clan_lib.vars.generate import (
def test_dependencies_as_files(temp_dir: Path) -> None:
from clan_cli.vars.generator import dependencies_as_dir
decrypted_dependencies = {
"gen_1": {
"var_1a": b"var_1a",
@@ -506,7 +506,6 @@ def test_generate_secret_var_password_store(
monkeypatch.setenv("PASSWORD_STORE_DIR", str(password_store_dir))
# Initialize password store as a git repository
import subprocess
subprocess.run(["git", "init"], cwd=password_store_dir, check=True)
subprocess.run(
@@ -613,8 +612,6 @@ def test_generate_secret_for_multiple_machines(
) -> None:
flake = flake_with_sops
from clan_lib.nix import nix_config
local_system = nix_config()["system"]
machine1_config = flake.machines["machine1"]
@@ -1101,8 +1098,6 @@ def test_create_sops_age_secrets(
# check private key exists
assert (flake.temporary_home / ".config" / "sops" / "age" / "keys.txt").is_file()
# it should still work, even if the keys already exist
import shutil
shutil.rmtree(flake.path / "sops" / "users" / "user")
cli.run(["vars", "keygen", "--flake", str(flake.path), "--user", "user"])
# check public key exists

View File

@@ -142,8 +142,6 @@ class StoreBase(ABC):
value: bytes,
is_migration: bool = False,
) -> list[Path]:
from clan_lib.machines.machines import Machine
changed_files: list[Path] = []
# if generator was switched from shared to per-machine or vice versa,
@@ -169,6 +167,8 @@ class StoreBase(ABC):
if generator.machine is None:
log_info = log.info
else:
from clan_lib.machines.machines import Machine # noqa: PLC0415
machine = Machine(name=generator.machine, flake=self.flake)
log_info = machine.info
if self.is_secret_store:

View File

@@ -32,13 +32,14 @@ def vars_status(
flake: Flake,
generator_name: None | str = None,
) -> VarStatus:
from clan_cli.vars.generator import Generator # noqa: PLC0415
machine = Machine(name=machine_name, flake=flake)
missing_secret_vars = []
missing_public_vars = []
# signals if a var needs to be updated (eg. needs re-encryption due to new users added)
unfixed_secret_vars = []
invalid_generators = []
from clan_cli.vars.generator import Generator
generators = Generator.get_machine_generators([machine.name], machine.flake)
if generator_name:

View File

@@ -2,6 +2,7 @@ import argparse
import logging
from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.vars.generator import Generator
from clan_lib.errors import ClanError
from clan_lib.flake import require_flake
from clan_lib.machines.machines import Machine
@@ -10,8 +11,6 @@ log = logging.getLogger(__name__)
def fix_vars(machine: Machine, generator_name: None | str = None) -> None:
from clan_cli.vars.generator import Generator
generators = Generator.get_machine_generators([machine.name], machine.flake)
if generator_name:
for generator in generators:

View File

@@ -1,23 +1,31 @@
import logging
import os
import shutil
import sys
from contextlib import ExitStack
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import TYPE_CHECKING
from clan_lib import bwrap
from clan_lib.cmd import RunOpts, run
from clan_lib.errors import ClanError
from clan_lib.nix import nix_test_store
from clan_lib.git import commit_files
from clan_lib.nix import nix_config, nix_shell, nix_test_store
from .check import check_vars
from .prompt import Prompt
from .prompt import Prompt, ask
from .var import Var
if TYPE_CHECKING:
from clan_lib.flake import Flake
from clan_lib.machines.machines import Machine
if TYPE_CHECKING:
from ._types import StoreBase
from clan_lib.machines.machines import Machine
log = logging.getLogger(__name__)
@@ -91,8 +99,6 @@ class Generator:
list[Generator]: A list of (unsorted) generators for the machine.
"""
from clan_lib.nix import nix_config
config = nix_config()
system = config["system"]
@@ -125,8 +131,6 @@ class Generator:
files_selector,
)
from clan_lib.machines.machines import Machine
machine = Machine(name=machine_name, flake=flake)
pub_store = machine.public_vars_store
sec_store = machine.secret_vars_store
@@ -207,8 +211,6 @@ class Generator:
if self._flake is None:
msg = "Flake cannot be None"
raise ClanError(msg)
from clan_lib.machines.machines import Machine
machine = Machine(name=self.machine, flake=self._flake)
output = Path(
machine.select(
@@ -226,8 +228,6 @@ class Generator:
if self._flake is None:
msg = "Flake cannot be None"
raise ClanError(msg)
from clan_lib.machines.machines import Machine
machine = Machine(name=self.machine, flake=self._flake)
return machine.select(
f'config.clan.core.vars.generators."{self.name}".validationHash',
@@ -250,8 +250,6 @@ class Generator:
Dictionary mapping generator names to their variable values
"""
from clan_lib.errors import ClanError
generators = self.get_machine_generators([machine.name], machine.flake)
result: dict[str, dict[str, bytes]] = {}
@@ -297,8 +295,6 @@ class Generator:
Dictionary mapping prompt names to their values
"""
from .prompt import ask
prompt_values: dict[str, str] = {}
for prompt in self.prompts:
var_id = f"{self.name}/{prompt.name}"
@@ -323,17 +319,6 @@ class Generator:
no_sandbox: Whether to disable sandboxing when executing the generator
"""
import os
import sys
from contextlib import ExitStack
from pathlib import Path
from tempfile import TemporaryDirectory
from clan_lib import bwrap
from clan_lib.cmd import RunOpts, run
from clan_lib.errors import ClanError
from clan_lib.git import commit_files
if prompt_values is None:
prompt_values = self.ask_prompts()
@@ -353,10 +338,6 @@ class Generator:
def bubblewrap_cmd(generator: str, tmpdir: Path) -> list[str]:
"""Helper function to create bubblewrap command."""
import shutil
from clan_lib.nix import nix_shell, nix_test_store
test_store = nix_test_store()
real_bash_path = Path("bash")
if os.environ.get("IN_NIX_SANDBOX"):
@@ -414,7 +395,7 @@ class Generator:
if sys.platform == "linux" and bwrap.bubblewrap_works():
cmd = bubblewrap_cmd(str(final_script), tmpdir)
elif sys.platform == "darwin":
from clan_lib.sandbox_exec import sandbox_exec_cmd
from clan_lib.sandbox_exec import sandbox_exec_cmd # noqa: PLC0415
cmd = stack.enter_context(sandbox_exec_cmd(str(final_script), tmpdir))
else:

View File

@@ -8,6 +8,7 @@ from tempfile import TemporaryDirectory
from clan_cli.vars._types import StoreBase
from clan_cli.vars.generator import Generator, Var
from clan_lib.cmd import Log, RunOpts
from clan_lib.flake import Flake
from clan_lib.ssh.host import Host
from clan_lib.ssh.upload import upload
@@ -162,8 +163,6 @@ class SecretStore(StoreBase):
if not git_hash:
return b""
from clan_cli.vars.generator import Generator
generators = Generator.get_machine_generators([machine], self.flake)
manifest = [
f"{generator.name}/{file.name}".encode()
@@ -179,8 +178,6 @@ class SecretStore(StoreBase):
if not local_hash:
return True
from clan_lib.cmd import Log, RunOpts
remote_hash = host.run(
[
"cat",
@@ -195,8 +192,6 @@ class SecretStore(StoreBase):
return local_hash != remote_hash.encode()
def populate_dir(self, machine: str, output_dir: Path, phases: list[str]) -> None:
from clan_cli.vars.generator import Generator
vars_generators = Generator.get_machine_generators([machine], self.flake)
if "users" in phases:
with tarfile.open(

View File

@@ -54,7 +54,7 @@ class SecretStore(StoreBase):
def ensure_machine_key(self, machine: str) -> None:
"""Ensure machine has sops keys initialized."""
# no need to generate keys if we don't manage secrets
from clan_cli.vars.generator import Generator
from clan_cli.vars.generator import Generator # noqa: PLC0415
vars_generators = Generator.get_machine_generators([machine], self.flake)
if not vars_generators:
@@ -141,7 +141,7 @@ class SecretStore(StoreBase):
"""
if generators is None:
from clan_cli.vars.generator import Generator
from clan_cli.vars.generator import Generator # noqa: PLC0415
generators = Generator.get_machine_generators([machine], self.flake)
file_found = False
@@ -219,7 +219,7 @@ class SecretStore(StoreBase):
return [store_folder]
def populate_dir(self, machine: str, output_dir: Path, phases: list[str]) -> None:
from clan_cli.vars.generator import Generator
from clan_cli.vars.generator import Generator # noqa: PLC0415
vars_generators = Generator.get_machine_generators([machine], self.flake)
if "users" in phases or "services" in phases:
@@ -291,7 +291,7 @@ class SecretStore(StoreBase):
)
def collect_keys_for_secret(self, machine: str, path: Path) -> set[sops.SopsKey]:
from clan_cli.secrets.secrets import (
from clan_cli.secrets.secrets import ( # noqa: PLC0415
collect_keys_for_path,
collect_keys_for_type,
)
@@ -352,10 +352,10 @@ class SecretStore(StoreBase):
ClanError: If the specified file_name is not found
"""
from clan_cli.secrets.secrets import update_keys
from clan_cli.secrets.secrets import update_keys # noqa: PLC0415
if generators is None:
from clan_cli.vars.generator import Generator
from clan_cli.vars.generator import Generator # noqa: PLC0415
generators = Generator.get_machine_generators([machine], self.flake)
file_found = False

View File

@@ -20,6 +20,7 @@ from clan_lib.async_run import get_current_thread_opkey
from clan_lib.errors import ClanError
from .serde import dataclass_to_dict, from_dict, sanitize_string
from .type_to_jsonschema import JSchemaTypeError, type_to_dict
log = logging.getLogger(__name__)
@@ -201,10 +202,6 @@ API.register(get_system_file)
return fn
def to_json_schema(self) -> dict[str, Any]:
from typing import get_type_hints
from .type_to_jsonschema import JSchemaTypeError, type_to_dict
api_schema: dict[str, Any] = {
"$comment": "An object containing API methods. ",
"type": "object",
@@ -268,8 +265,6 @@ API.register(get_system_file)
return api_schema
def get_method_argtype(self, method_name: str, arg_name: str) -> Any:
from inspect import signature
func = self._registry.get(method_name, None)
if not func:
msg = f"API Method {method_name} not found in registry. Available methods: {list(self._registry.keys())}"
@@ -313,9 +308,9 @@ def load_in_all_api_functions() -> None:
We have to make sure python loads every wrapped function at least once.
This is done by importing all modules from the clan_lib and clan_cli packages.
"""
import clan_cli
import clan_cli # noqa: PLC0415 # Avoid circular imports - many modules import from clan_lib.api
import clan_lib
import clan_lib # noqa: PLC0415 # Avoid circular imports - many modules import from clan_lib.api
import_all_modules_from_package(clan_lib)
import_all_modules_from_package(clan_cli)

View File

@@ -4,8 +4,7 @@ from pathlib import Path
from typing import Any, Literal
from clan_lib.cmd import RunOpts, run
from clan_lib.flake import Flake
from clan_lib.nix import nix_shell
from clan_lib.flake.flake import Flake
from . import API
@@ -89,6 +88,8 @@ def list_system_storage_devices() -> Blockdevices:
A list of detected block devices with metadata like size, path, type, etc.
"""
from clan_lib.nix import nix_shell # noqa: PLC0415
cmd = nix_shell(
["util-linux"],
[
@@ -123,7 +124,7 @@ def get_clan_directory_relative(flake: Flake) -> str:
ClanError: If the flake evaluation fails or directories cannot be found
"""
from clan_lib.dirs import get_clan_directories
from clan_lib.dirs import get_clan_directories # noqa: PLC0415
_, relative_dir = get_clan_directories(flake)
return relative_dir

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, Literal, TypedDict
@@ -334,8 +335,6 @@ def test_literal_field() -> None:
def test_enum_roundtrip() -> None:
from enum import Enum
class MyEnum(Enum):
FOO = "abc"
BAR = 2

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from enum import Enum
# Functions to test
from clan_lib.api import (
@@ -124,8 +125,6 @@ def test_filters_null_fields() -> None:
def test_custom_enum() -> None:
from enum import Enum
class CustomEnum(Enum):
FOO = "foo"
BAR = "bar"

View File

@@ -1,5 +1,6 @@
from dataclasses import dataclass, field
from typing import Any, NotRequired, Required
from enum import Enum
from typing import Any, Generic, NotRequired, Required, TypedDict, TypeVar
import pytest
@@ -27,8 +28,6 @@ def test_simple_primitives() -> None:
def test_enum_type() -> None:
from enum import Enum
class Color(Enum):
RED = "red"
GREEN = "green"
@@ -224,8 +223,6 @@ def test_nested_open_dicts() -> None:
def test_type_variables() -> None:
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass
@@ -254,8 +251,6 @@ def test_type_variables() -> None:
def test_type_variable_nested_scopes() -> None:
# Define two type variables with the same name "T" but in different scopes
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass
@@ -284,8 +279,6 @@ def test_type_variable_nested_scopes() -> None:
def test_total_typed_dict() -> None:
from typing import TypedDict
class ExampleTypedDict(TypedDict):
name: str
value: NotRequired[int]
@@ -314,8 +307,6 @@ def test_total_typed_dict() -> None:
def test_open_typed_dict() -> None:
from typing import TypedDict
class ExampleTypedDict(TypedDict, total=False):
name: Required[str]
value: int

View File

@@ -1,3 +1,4 @@
import json
import logging
import os
import sys
@@ -6,7 +7,9 @@ from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Protocol
from clan_lib.cmd import run
from clan_lib.errors import ClanError
from clan_lib.nix import nix_eval
if TYPE_CHECKING:
from clan_lib.flake import Flake
@@ -198,12 +201,6 @@ def get_clan_directories(flake: "Flake") -> tuple[str, str]:
ClanError: If the flake evaluation fails or directories cannot be found
"""
import json
from pathlib import Path
from clan_lib.cmd import run
from clan_lib.nix import nix_eval
# Get the source directory from nix store
root_directory = flake.select("sourceInfo")

View File

@@ -11,7 +11,16 @@ from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any
from clan_lib.cmd import Log, RunOpts, run
from clan_lib.dirs import select_source, user_cache_dir
from clan_lib.errors import ClanCmdError, ClanError
from clan_lib.nix import (
nix_build,
nix_command,
nix_config,
nix_metadata,
nix_test_store,
)
log = logging.getLogger(__name__)
@@ -851,11 +860,6 @@ class Flake:
def prefetch(self) -> None:
"""Loads the flake into the store and populates self.store_path and self.hash such that the flake can evaluate locally and offline"""
from clan_lib.cmd import RunOpts, run
from clan_lib.nix import (
nix_command,
)
if self.nix_options is None:
self.nix_options = []
@@ -895,11 +899,6 @@ class Flake:
This method is used to refresh the cache by reloading it from the flake.
"""
from clan_lib.dirs import user_cache_dir
from clan_lib.nix import (
nix_metadata,
)
self.prefetch()
self._cache = FlakeCache()
@@ -951,14 +950,6 @@ class Flake:
AssertionError: If the cache or flake cache path is not properly initialized.
"""
from clan_lib.cmd import Log, RunOpts, run
from clan_lib.dirs import select_source
from clan_lib.nix import (
nix_build,
nix_config,
nix_test_store,
)
if self._cache is None:
self.invalidate_cache()
if self._cache is None:
@@ -1125,8 +1116,6 @@ class Flake:
apply: Optional function to apply to the result
"""
from clan_lib.nix import nix_config
config = nix_config()
system = config["system"]

View File

@@ -360,11 +360,6 @@ def test_store_path_with_line_numbers_not_wrapped() -> None:
def test_store_reference_helpers() -> None:
"""Test the store reference helper functions."""
from clan_lib.flake.flake import (
find_store_references,
is_pure_store_path,
)
# Test find_store_references
assert find_store_references("/nix/store/abc123-pkg") == ["/nix/store/abc123-pkg"]
assert find_store_references("/nix/store/abc123-file.nix:42") == [

View File

@@ -7,6 +7,7 @@ from tempfile import TemporaryDirectory
from typing import Any, Literal
from clan_cli.facts.generate import generate_facts
from clan_cli.vars.generator import Generator
from clan_cli.vars.upload import populate_secret_vars
from clan_lib.api import API
@@ -116,8 +117,6 @@ def run_machine_flash(
"users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}},
}
from clan_cli.vars.generator import Generator
for generator in Generator.get_machine_generators(
[machine.name], machine.flake
):

View File

@@ -1,3 +1,5 @@
import importlib.util
import sys
import tempfile
from pathlib import Path
from textwrap import dedent
@@ -52,8 +54,6 @@ def test_import_with_source(tmp_path: Path) -> None:
)
# Add the temp directory to sys.path
import sys
sys.path.insert(0, str(tmp_path))
try:
@@ -130,9 +130,6 @@ def test_import_with_source_with_args() -> None:
temp_file = Path(f.name)
# Import module dynamically
import importlib.util
import sys
spec = importlib.util.spec_from_file_location("temp_module", temp_file)
assert spec is not None
assert spec.loader is not None

View File

@@ -5,7 +5,6 @@ from contextlib import contextmanager
from pathlib import Path
from typing import Any
from clan_lib.dirs import user_history_file
from clan_lib.jsonrpc import ClanJSONEncoder
@@ -19,11 +18,15 @@ def locked_open(filename: Path, mode: str = "r") -> Generator:
def write_history_file(data: Any) -> None:
from clan_lib.dirs import user_history_file # noqa: PLC0415
with locked_open(user_history_file(), "w+") as f:
f.write(json.dumps(data, cls=ClanJSONEncoder, indent=4))
def read_history_file() -> list[dict]:
from clan_lib.dirs import user_history_file # noqa: PLC0415
with locked_open(user_history_file(), "r") as f:
content: str = f.read()
parsed: list[dict] = json.loads(content)

View File

@@ -4,11 +4,13 @@ Tests are based on actual usage patterns from example_usage.py and api.py.
"""
import datetime
import tempfile
from pathlib import Path
import pytest
from clan_lib.log_manager import (
LogFile,
LogGroupConfig,
LogManager,
is_correct_day_format,
@@ -472,8 +474,6 @@ class TestLogFileSorting:
configured_log_manager: LogManager,
) -> None:
"""Test that LogFiles are sorted by datetime (newest first)."""
from clan_lib.log_manager import LogFile
# Create LogFiles with different times (same date)
newer_file = LogFile(
op_key="test_op",
@@ -508,8 +508,6 @@ class TestLogFileSorting:
configured_log_manager: LogManager,
) -> None:
"""Test that LogFiles are sorted by date (newer dates first)."""
from clan_lib.log_manager import LogFile
# Create LogFiles with different dates
newer_date_file = LogFile(
op_key="test_op",
@@ -543,8 +541,6 @@ class TestLogFileSorting:
configured_log_manager: LogManager,
) -> None:
"""Test that LogFiles with same datetime are sorted by group name (alphabetical)."""
from clan_lib.log_manager import LogFile
# Create LogFiles with same datetime but different groups
group_a_file = LogFile(
op_key="test_op",
@@ -579,8 +575,6 @@ class TestLogFileSorting:
configured_log_manager: LogManager,
) -> None:
"""Test that LogFiles with same datetime and group are sorted by func_name (alphabetical)."""
from clan_lib.log_manager import LogFile
# Create LogFiles with same datetime and group but different func_names
func_a_file = LogFile(
op_key="test_op",
@@ -614,8 +608,6 @@ class TestLogFileSorting:
configured_log_manager: LogManager,
) -> None:
"""Test that LogFiles with same datetime, group, and func_name are sorted by op_key (alphabetical)."""
from clan_lib.log_manager import LogFile
# Create LogFiles identical except for op_key
op_a_file = LogFile(
op_key="op_a", # Should sort first alphabetically
@@ -649,8 +641,6 @@ class TestLogFileSorting:
configured_log_manager: LogManager,
) -> None:
"""Test complex sorting with multiple LogFiles demonstrating full sort order."""
from clan_lib.log_manager import LogFile
# Create multiple files with different characteristics
files = [
# Oldest datetime, should be last
@@ -813,8 +803,6 @@ class TestLogFileSorting:
"""Test that list_log_days returns days sorted newest first."""
del configured_log_manager # Unused but kept for API compatibility
# Create log files on different days by manipulating the date
import tempfile
# Create files with different dates manually to test sorting
with tempfile.TemporaryDirectory() as tmp_dir:
base_dir = Path(tmp_dir)

View File

@@ -33,7 +33,7 @@ class Machine:
def get_inv_machine(self) -> "InventoryMachine":
# Import on demand to avoid circular imports
from clan_lib.machines.actions import get_machine
from clan_lib.machines.actions import get_machine # noqa: PLC0415
return get_machine(self.flake, self.name)
@@ -121,7 +121,7 @@ class Machine:
return self.flake.path
def target_host(self) -> Remote:
from clan_lib.network.network import get_best_remote
from clan_lib.network.network import get_best_remote # noqa: PLC0415
with get_best_remote(self) as remote:
return remote

View File

@@ -42,7 +42,7 @@ def _suggest_similar_names(
def get_available_machines(flake: Flake) -> list[str]:
from clan_lib.machines.list import list_machines
from clan_lib.machines.list import list_machines # noqa: PLC0415
machines = list_machines(flake)
return list(machines.keys())

View File

@@ -34,7 +34,7 @@ class Peer:
_var: dict[str, str] = self._host["var"]
machine_name = _var["machine"]
generator = _var["generator"]
from clan_lib.machines.machines import Machine
from clan_lib.machines.machines import Machine # noqa: PLC0415
machine = Machine(name=machine_name, flake=self.flake)
var = get_machine_var(

View File

@@ -47,8 +47,6 @@ class NetworkTechnology(NetworkTechnologyBase):
yield network
def remote(self, peer: Peer) -> "Remote":
from clan_lib.ssh.remote import Remote
return Remote(
address=peer.host,
command_prefix=peer.name,

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import logging
import socket
import time
from collections.abc import Iterator
from contextlib import contextmanager
@@ -70,8 +71,6 @@ class TorCheck:
def tor_online_test(proxy_port: int) -> None:
"""Tests if Tor is online by checking if we can establish a SOCKS5 connection."""
import socket
# Try to establish a SOCKS5 handshake with the Tor proxy
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2.0) # Short timeout for local connection

View File

@@ -8,7 +8,6 @@ from pathlib import Path
from typing import Any
from clan_lib.cmd import run
from clan_lib.dirs import nixpkgs_flake, nixpkgs_source
from clan_lib.errors import ClanError
from clan_lib.locked_open import locked_open
@@ -89,6 +88,8 @@ def nix_eval(flags: list[str]) -> list[str]:
],
)
if os.environ.get("IN_NIX_SANDBOX"):
from clan_lib.dirs import nixpkgs_source # noqa: PLC0415
return [
*default_flags,
"--override-input",
@@ -168,6 +169,9 @@ def nix_shell(packages: list[str], cmd: list[str]) -> list[str]:
] + [package for package in packages if "#" in package]
if not missing_packages:
return cmd
from clan_lib.dirs import nixpkgs_flake # noqa: PLC0415
return [
*nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]),
*missing_packages,

View File

@@ -459,12 +459,12 @@ class Remote:
self,
opts: "ConnectionOptions | None" = None,
) -> None:
from clan_lib.network.check import check_machine_ssh_reachable
from clan_lib.network.check import check_machine_ssh_reachable # noqa: PLC0415
return check_machine_ssh_reachable(self, opts)
def check_machine_ssh_login(self) -> None:
from clan_lib.network.check import check_machine_ssh_login
from clan_lib.network.check import check_machine_ssh_login # noqa: PLC0415
return check_machine_ssh_login(self)
@@ -521,7 +521,6 @@ def _parse_ssh_uri(
raise ClanError(msg)
hostname = result.hostname
port = result.port
from clan_lib.ssh.remote import Remote
return Remote(
address=hostname,

View File

@@ -1,6 +1,7 @@
import logging
from collections.abc import Callable
from clan_cli.vars import graph
from clan_cli.vars.generator import Generator, GeneratorKey
from clan_cli.vars.graph import minimal_closure, requested_closure
from clan_cli.vars.migration import check_can_migrate, migrate_files
@@ -31,8 +32,6 @@ def get_generators(
List of generators based on the specified selection and closure mode.
"""
from clan_cli.vars import graph
machine_names = [machine.name for machine in machines]
vars_generators = Generator.get_machine_generators(
machine_names,

View File

@@ -36,7 +36,7 @@ class MPProcess:
def _set_proc_name(name: str) -> None:
if sys.platform != "linux":
return
import ctypes
import ctypes # noqa: PLC0415
# Define the prctl function with the appropriate arguments and return type
libc = ctypes.CDLL("libc.so.6")

View File

@@ -33,6 +33,15 @@ from gi.repository import GdkPixbuf, Gio, GLib, Gtk
from clan_vm_manager.assets import loc
# Windows-specific imports
if sys.platform == "win32":
from ctypes import byref, sizeof, windll # type: ignore[attr-defined]
else:
# Create dummy objects for type checking on non-Windows systems
windll = None
byref = None # type: ignore[assignment]
sizeof = None # type: ignore[assignment]
# DUMMY IMPLEMENTATION
################################################
@@ -747,12 +756,12 @@ class Win32Implementation(BaseImplementation):
SM_CXSMICON = 49
if sys.platform == "win32":
from ctypes import Structure
from ctypes import Structure # noqa: PLC0415
class WNDCLASSW(Structure):
"""Windows class structure for window registration."""
from ctypes import CFUNCTYPE, wintypes
from ctypes import CFUNCTYPE, wintypes # noqa: PLC0415
LPFN_WND_PROC = CFUNCTYPE(
wintypes.INT,
@@ -777,7 +786,7 @@ class Win32Implementation(BaseImplementation):
class MENUITEMINFOW(Structure):
"""Windows menu item information structure."""
from ctypes import wintypes
from ctypes import wintypes # noqa: PLC0415
_fields_: ClassVar = [
("cb_size", wintypes.UINT),
@@ -797,7 +806,7 @@ class Win32Implementation(BaseImplementation):
class NOTIFYICONDATAW(Structure):
"""Windows notification icon data structure."""
from ctypes import wintypes
from ctypes import wintypes # noqa: PLC0415
_fields_: ClassVar = [
("cb_size", wintypes.DWORD),
@@ -818,8 +827,6 @@ class Win32Implementation(BaseImplementation):
]
def __init__(self, application: Gtk.Application) -> None:
from ctypes import windll # type: ignore[attr-defined]
super().__init__(application)
self._window_class: Any = None
@@ -827,42 +834,45 @@ class Win32Implementation(BaseImplementation):
self._notify_id = None
self._h_icon = None
self._menu = None
self._wm_taskbarcreated = windll.user32.RegisterWindowMessageW("TaskbarCreated")
if sys.platform == "win32":
self._wm_taskbarcreated = windll.user32.RegisterWindowMessageW(
"TaskbarCreated"
) # type: ignore[attr-defined]
self._register_class()
self._create_window()
self.update_icon()
def _register_class(self) -> None:
from ctypes import byref, windll # type: ignore[attr-defined]
if sys.platform != "win32":
return
self._window_class = self.WNDCLASSW( # type: ignore[attr-defined]
style=(self.CS_VREDRAW | self.CS_HREDRAW),
lpfn_wnd_proc=self.WNDCLASSW.LPFN_WND_PROC(self.on_process_window_message), # type: ignore[attr-defined]
h_cursor=windll.user32.LoadCursorW(0, self.IDC_ARROW),
h_cursor=windll.user32.LoadCursorW(0, self.IDC_ARROW), # type: ignore[attr-defined]
hbr_background=self.COLOR_WINDOW,
lpsz_class_name=self.WINDOW_CLASS_NAME,
)
windll.user32.RegisterClassW(byref(self._window_class))
windll.user32.RegisterClassW(byref(self._window_class)) # type: ignore[attr-defined]
def _unregister_class(self):
if self._window_class is None:
if self._window_class is None or sys.platform != "win32":
return
from ctypes import windll
windll.user32.UnregisterClassW(
windll.user32.UnregisterClassW( # type: ignore[attr-defined]
self.WINDOW_CLASS_NAME,
self._window_class.h_instance,
)
self._window_class = None
def _create_window(self) -> None:
from ctypes import windll # type: ignore[attr-defined]
if sys.platform != "win32":
return
style = self.WS_OVERLAPPED | self.WS_SYSMENU
self._h_wnd = windll.user32.CreateWindowExW(
self._h_wnd = windll.user32.CreateWindowExW( # type: ignore[attr-defined]
0,
self.WINDOW_CLASS_NAME,
self.WINDOW_CLASS_NAME,
@@ -877,15 +887,13 @@ class Win32Implementation(BaseImplementation):
None,
)
windll.user32.UpdateWindow(self._h_wnd)
windll.user32.UpdateWindow(self._h_wnd) # type: ignore[attr-defined]
def _destroy_window(self):
if self._h_wnd is None:
if self._h_wnd is None or sys.platform != "win32":
return
from ctypes import windll
windll.user32.DestroyWindow(self._h_wnd)
windll.user32.DestroyWindow(self._h_wnd) # type: ignore[attr-defined]
self._h_wnd = None
def _load_ico_buffer(self, icon_name, icon_size):
@@ -922,10 +930,11 @@ class Win32Implementation(BaseImplementation):
return ico_buffer
def _load_h_icon(self, icon_name):
from ctypes import windll
if sys.platform != "win32":
return None
# Attempt to load custom icons first
icon_size = windll.user32.GetSystemMetrics(self.SM_CXSMICON)
icon_size = windll.user32.GetSystemMetrics(self.SM_CXSMICON) # type: ignore[attr-defined]
ico_buffer = self._load_ico_buffer(
icon_name.replace(f"{pynicotine.__application_id__}-", "nplus-tray-"),
icon_size,
@@ -937,7 +946,7 @@ class Win32Implementation(BaseImplementation):
with tempfile.NamedTemporaryFile(delete=False) as file_handle:
file_handle.write(ico_buffer)
return windll.user32.LoadImageA(
return windll.user32.LoadImageA( # type: ignore[attr-defined]
0,
encode_path(file_handle.name),
self.IMAGE_ICON,
@@ -947,16 +956,15 @@ class Win32Implementation(BaseImplementation):
)
def _destroy_h_icon(self):
from ctypes import windll
if sys.platform != "win32" or not self._h_icon:
return
if self._h_icon:
windll.user32.DestroyIcon(self._h_icon)
windll.user32.DestroyIcon(self._h_icon) # type: ignore[attr-defined]
self._h_icon = None
def _update_notify_icon(self, title="", message="", icon_name=None):
# pylint: disable=attribute-defined-outside-init,no-member
if self._h_wnd is None:
if sys.platform != "win32" or self._h_wnd is None:
return
if icon_name:
@@ -967,8 +975,6 @@ class Win32Implementation(BaseImplementation):
# When disabled by user, temporarily show tray icon when displaying a notification
return
from ctypes import byref, sizeof, windll
action = self.NIM_MODIFY
if self._notify_id is None:
@@ -1004,23 +1010,24 @@ class Win32Implementation(BaseImplementation):
ellipsize=True,
)
windll.shell32.Shell_NotifyIconW(action, byref(self._notify_id))
windll.shell32.Shell_NotifyIconW(action, byref(self._notify_id)) # type: ignore[attr-defined]
def _remove_notify_icon(self):
from ctypes import byref, windll
if sys.platform != "win32":
return
if self._notify_id:
windll.shell32.Shell_NotifyIconW(self.NIM_DELETE, byref(self._notify_id))
windll.shell32.Shell_NotifyIconW(self.NIM_DELETE, byref(self._notify_id)) # type: ignore[attr-defined]
self._notify_id = None
if self._menu:
windll.user32.DestroyMenu(self._menu)
windll.user32.DestroyMenu(self._menu) # type: ignore[attr-defined]
self._menu = None
def _serialize_menu_item(self, item):
# pylint: disable=attribute-defined-outside-init,no-member
from ctypes import sizeof
if sys.platform != "win32":
return None
item_info = self.MENUITEMINFOW(cb_size=sizeof(self.MENUITEMINFOW))
w_id = item["id"]
@@ -1048,38 +1055,42 @@ class Win32Implementation(BaseImplementation):
return item_info
def _show_menu(self):
from ctypes import byref, windll, wintypes
if sys.platform != "win32":
return
from ctypes import wintypes # noqa: PLC0415
if self._menu is None:
self.update_menu()
pos = wintypes.POINT()
windll.user32.GetCursorPos(byref(pos))
windll.user32.GetCursorPos(byref(pos)) # type: ignore[attr-defined]
# PRB: Menus for Notification Icons Do Not Work Correctly
# https://web.archive.org/web/20121015064650/http://support.microsoft.com/kb/135788
windll.user32.SetForegroundWindow(self._h_wnd)
windll.user32.TrackPopupMenu(self._menu, 0, pos.x, pos.y, 0, self._h_wnd, None)
windll.user32.PostMessageW(self._h_wnd, self.WM_NULL, 0, 0)
windll.user32.SetForegroundWindow(self._h_wnd) # type: ignore[attr-defined]
windll.user32.TrackPopupMenu(self._menu, 0, pos.x, pos.y, 0, self._h_wnd, None) # type: ignore[attr-defined]
windll.user32.PostMessageW(self._h_wnd, self.WM_NULL, 0, 0) # type: ignore[attr-defined]
def update_menu(self):
from ctypes import byref, windll
if sys.platform != "win32":
return
if self._menu is None:
self._menu = windll.user32.CreatePopupMenu()
self._menu = windll.user32.CreatePopupMenu() # type: ignore[attr-defined]
for item in self.menu_items.values():
item_id = item["id"]
item_info = self._serialize_menu_item(item)
if not windll.user32.SetMenuItemInfoW(
if not windll.user32.SetMenuItemInfoW( # type: ignore[attr-defined]
self._menu,
item_id,
False,
byref(item_info),
):
windll.user32.InsertMenuItemW(
windll.user32.InsertMenuItemW( # type: ignore[attr-defined]
self._menu,
item_id,
False,
@@ -1093,7 +1104,10 @@ class Win32Implementation(BaseImplementation):
self._update_notify_icon(title=title, message=message)
def on_process_window_message(self, h_wnd, msg, w_param, l_param):
from ctypes import windll, wintypes
if sys.platform != "win32":
return 0
from ctypes import wintypes # noqa: PLC0415
if msg == self.WM_TRAYICON:
if l_param == self.WM_RBUTTONUP:
@@ -1124,7 +1138,7 @@ class Win32Implementation(BaseImplementation):
self._remove_notify_icon()
self._update_notify_icon()
return windll.user32.DefWindowProcW(
return windll.user32.DefWindowProcW( # type: ignore[attr-defined]
wintypes.HWND(h_wnd),
msg,
wintypes.WPARAM(w_param),

View File

@@ -65,8 +65,6 @@ class TestFlake(Flake):
apply: Optional function to apply to the result
"""
from clan_lib.nix import nix_config
config = nix_config()
system = config["system"]
test_system = system

View File

@@ -66,8 +66,6 @@ lint.ignore = [
"TRY301",
"FBT003",
"INP001",
# TODO: fix later
"PLC0415",
]
[tool.ruff.lint.per-file-ignores]