From 72db20c827a5e2b2afca6072a620493e5f4981d6 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Thu, 27 Mar 2025 09:50:58 -0400 Subject: [PATCH 1/8] feat: add Mypy type-checking to CI workflow and update mypy configuration --- .github/workflows/test.yml | 9 +++++---- pyproject.toml | 6 ++++-- src/snakemake_interface_common/py.typed | 0 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 src/snakemake_interface_common/py.typed diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce99fd5..e1b9533 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,10 +35,11 @@ jobs: if: always() run: | pixi run --environment dev lint --diff - # - name: Mypy - # if: always() - # run: | - # pixi run --environment dev type-check + + - name: Mypy + if: always() + run: | + pixi run --environment dev type-check - name: Collect QC run: echo "All quality control checks passed" diff --git a/pyproject.toml b/pyproject.toml index 054f892..3c4bbf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ platforms = ["osx-arm64", "linux-64"] [tool.pixi.pypi-dependencies] - [tool.pixi.environments] dev = { features = ["dev"] } publish = { features = ["publish"] } @@ -52,12 +51,15 @@ lint.ignore = ["E721"] disallow_untyped_defs = true warn_no_return = true - [[tool.mypy.overrides]] # TODO:: figure out expected types for the TestRegistryBase class module = "snakemake_interface_common.plugin_registry.tests" ignore_errors = true +[[tool.mypy.overrides]] +module = "argparse_dataclass" +ignore_missing_imports = true + [tool.pixi.feature.dev.tasks.test] cmd = [ "pytest", diff --git a/src/snakemake_interface_common/py.typed b/src/snakemake_interface_common/py.typed new file mode 100644 index 0000000..e69de29 From f81f2c59df7e7f5029c4011b7f3c97b6fb35ad81 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Thu, 27 Mar 2025 09:51:36 -0400 Subject: [PATCH 2/8] fix: enhance WorkflowError class with type hints and improve argument handling --- src/snakemake_interface_common/exceptions.py | 17 ++++++++++------- src/snakemake_interface_common/logging.py | 8 ++++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/snakemake_interface_common/exceptions.py b/src/snakemake_interface_common/exceptions.py index 68c95fd..a33cf3e 100644 --- a/src/snakemake_interface_common/exceptions.py +++ b/src/snakemake_interface_common/exceptions.py @@ -3,10 +3,9 @@ __email__ = "johannes.koester@uni-due.de" __license__ = "MIT" -from pathlib import Path import sys import textwrap -from typing import Optional +from typing import Optional, Any from snakemake_interface_common.rules import RuleInterface @@ -16,7 +15,11 @@ class ApiError(Exception): class WorkflowError(Exception): - def format_arg(self, arg): + lineno: Optional[int] + snakefile: Optional[str] + rule: Optional[RuleInterface] + + def format_arg(self, arg: object) -> str: if isinstance(arg, str): return arg elif isinstance(arg, WorkflowError): @@ -38,9 +41,9 @@ def format_arg(self, arg): def __init__( self, - *args, + *args: Any, lineno: Optional[int] = None, - snakefile: Optional[Path] = None, + snakefile: Optional[str] = None, rule: Optional[RuleInterface] = None, ): if rule is not None: @@ -55,12 +58,12 @@ def __init__( if args and isinstance(args[0], str): spec = self._get_spec(self) if spec: - args = [f"{args[0]} ({spec})"] + list(args[1:]) + args = tuple([f"{args[0]} ({spec})"] + list(args[1:])) super().__init__("\n".join(self.format_arg(arg) for arg in args)) @classmethod - def _get_spec(cls, exc): + def _get_spec(cls, exc: "WorkflowError") -> str: spec = "" if exc.rule is not None: spec += f"rule {exc.rule.name}" diff --git a/src/snakemake_interface_common/logging.py b/src/snakemake_interface_common/logging.py index 4b720e0..3f3e627 100644 --- a/src/snakemake_interface_common/logging.py +++ b/src/snakemake_interface_common/logging.py @@ -2,10 +2,14 @@ __copyright__ = "Copyright 2023, Johannes Köster" __email__ = "johannes.koester@uni-due.de" __license__ = "MIT" +from typing import TYPE_CHECKING +if TYPE_CHECKING: + import logging -def get_logger(): + +def get_logger() -> "logging.Logger": """Retrieve the logger singleton from snakemake.""" - from snakemake.logging import logger + from snakemake.logging import logger # type: ignore return logger From 43208288eb32aa9568a5f43244045ec7adbc02c7 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Thu, 27 Mar 2025 09:51:54 -0400 Subject: [PATCH 3/8] fix: snakefile is expected to be a str not a Path --- src/snakemake_interface_common/rules.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/snakemake_interface_common/rules.py b/src/snakemake_interface_common/rules.py index 27f210e..fb1e247 100644 --- a/src/snakemake_interface_common/rules.py +++ b/src/snakemake_interface_common/rules.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from pathlib import Path class RuleInterface(ABC): @@ -13,4 +12,4 @@ def lineno(self) -> int: ... @property @abstractmethod - def snakefile(self) -> Path: ... + def snakefile(self) -> str: ... From 73533c434532395880499c3bcaaf19d6d7355418 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Thu, 27 Mar 2025 09:52:50 -0400 Subject: [PATCH 4/8] refactor: add type hints to utility functions and methods for improved clarity --- src/snakemake_interface_common/utils.py | 31 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/snakemake_interface_common/utils.py b/src/snakemake_interface_common/utils.py index 25f25a1..c771f19 100644 --- a/src/snakemake_interface_common/utils.py +++ b/src/snakemake_interface_common/utils.py @@ -2,9 +2,10 @@ from datetime import datetime import os import subprocess as sp +from typing import Any, Callable, Optional, Tuple -def not_iterable(value): +def not_iterable(value: Any) -> bool: return ( isinstance(value, str) or isinstance(value, dict) @@ -16,15 +17,15 @@ class lazy_property(property): __slots__ = ["method", "cached", "__doc__"] @staticmethod - def clean(instance, method): + def clean(instance: Any, method: str) -> None: delattr(instance, method) - def __init__(self, method): + def __init__(self, method: Callable) -> None: self.method = method self.cached = f"_{method.__name__}" super().__init__(method, doc=method.__doc__) - def __get__(self, instance, owner): + def __get__(self, instance: Any, owner: type | None = None) -> Any: cached = ( getattr(instance, self.cached) if hasattr(instance, self.cached) else None ) @@ -35,9 +36,21 @@ def __get__(self, instance, owner): return value -def lutime(f, times) -> bool: +def lutime(f: str, times: Optional[Tuple[float, float]]) -> bool: """Set utime for a file or symlink. Do not follow symlink. - Return True if successful. + + Parameters + ---------- + f : str + The file or symlink. + times : Optional[Tuple[float, float]] + The access and modification times (atime, mtime) in seconds. + + Returns + ------- + bool + True if successful. + False if the system command fails. """ # In some cases, we have a platform where os.supports_follow_symlink includes # stat() but not utime(). This leads to an anomaly. In any case we never want @@ -53,7 +66,7 @@ def lutime(f, times) -> bool: # try the system command if times: - def fmt_time(sec): + def fmt_time(sec: float) -> str: return datetime.fromtimestamp(sec).strftime("%Y%m%d%H%M.%S") atime, mtime = times @@ -69,10 +82,10 @@ def fmt_time(sec): if os.chmod in os.supports_follow_symlinks: - def lchmod(f, mode): + def lchmod(f: str, mode: int) -> None: os.chmod(f, mode, follow_symlinks=False) else: - def lchmod(f, mode): + def lchmod(f: str, mode: int) -> None: os.chmod(f, mode) From b2cb27d70f1df026aad99579fec60689520cc29d Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Thu, 27 Mar 2025 09:52:58 -0400 Subject: [PATCH 5/8] refactor: add type hints to AttributeType properties and methods for improved clarity --- .../plugin_registry/attribute_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/snakemake_interface_common/plugin_registry/attribute_types.py b/src/snakemake_interface_common/plugin_registry/attribute_types.py index d5da7ac..d576a99 100644 --- a/src/snakemake_interface_common/plugin_registry/attribute_types.py +++ b/src/snakemake_interface_common/plugin_registry/attribute_types.py @@ -20,12 +20,12 @@ class AttributeType: kind: AttributeKind = AttributeKind.OBJECT @property - def is_optional(self): + def is_optional(self) -> bool: return self.mode == AttributeMode.OPTIONAL @property - def is_class(self): + def is_class(self) -> bool: return self.kind == AttributeKind.CLASS - def into_required(self): + def into_required(self) -> "AttributeType": return AttributeType(cls=self.cls, mode=AttributeMode.REQUIRED, kind=self.kind) From e4e3ed342e283a4c6d3233ba8032a428c097d4e8 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Thu, 27 Mar 2025 09:53:09 -0400 Subject: [PATCH 6/8] refactor: enhance type hints across plugin registry for improved type safety and clarity --- .../plugin_registry/plugin.py | 73 ++++++++++++------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/snakemake_interface_common/plugin_registry/plugin.py b/src/snakemake_interface_common/plugin_registry/plugin.py index d8d5030..79d6008 100644 --- a/src/snakemake_interface_common/plugin_registry/plugin.py +++ b/src/snakemake_interface_common/plugin_registry/plugin.py @@ -5,10 +5,22 @@ from abc import ABC, abstractmethod from collections import defaultdict -from dataclasses import field, fields +from dataclasses import field, fields, Field from dataclasses import MISSING, dataclass from pathlib import Path -from typing import Any, Dict, List, Optional, Sequence, Type, Union +from typing import ( + Any, + Dict, + List, + Optional, + Sequence, + Type, + Union, + TYPE_CHECKING, + TypeVar, + Generic, + cast, +) import typing from snakemake_interface_common.exceptions import WorkflowError @@ -17,6 +29,8 @@ dataclass_field_to_argument_args, ) +if TYPE_CHECKING: + from argparse import ArgumentParser # Valid Argument types (to distinguish from empty dataclasses) ArgTypes = (str, int, float, bool, list, Path) @@ -25,7 +39,7 @@ class SettingsBase: """Base class for plugin settings.""" - def get_items_by_category(self, category: str): + def get_items_by_category(self, category: str) -> typing.Iterator[tuple[str, Any]]: """Yield all items (name, value) of the given group (as defined by the) optional subgroup field in the metadata. """ @@ -36,9 +50,11 @@ def get_items_by_category(self, category: str): @dataclass class TaggedSettings: - _inner: Dict[str, SettingsBase] = field(default_factory=dict, init=False) + _inner: Dict[str | None, SettingsBase] = field(default_factory=dict, init=False) - def register_settings(self, settings: SettingsBase, tag: Optional[str] = None): + def register_settings( + self, settings: SettingsBase, tag: Optional[str] = None + ) -> None: self._inner[tag] = settings def get_settings(self, tag: Optional[str] = None) -> Optional[SettingsBase]: @@ -48,17 +64,20 @@ def get_settings(self, tag: Optional[str] = None) -> Optional[SettingsBase]: settings = self._inner.get(None) return settings - def get_field_settings(self, field_name: str) -> Dict[str, Sequence[Any]]: + def get_field_settings(self, field_name: str) -> Dict[str | None, Sequence[Any]]: """Return a dictionary of tag -> value for the given field name.""" return { tag: getattr(settings, field_name) for tag, settings in self._inner.items() } - def __iter__(self): + def __iter__(self) -> typing.Iterator[SettingsBase]: return iter(self._inner.values()) -class PluginBase(ABC): +TSettingsBase = TypeVar("TSettingsBase", bound="SettingsBase") + + +class PluginBase(ABC, Generic[TSettingsBase]): @property @abstractmethod def name(self) -> str: ... @@ -69,13 +88,13 @@ def cli_prefix(self) -> str: ... @property @abstractmethod - def settings_cls(self) -> Type[SettingsBase]: ... + def settings_cls(self) -> Optional[Type[TSettingsBase]]: ... @property def support_tagged_values(self) -> bool: return False - def has_settings_cls(self): + def has_settings_cls(self) -> bool: """Determine if a plugin defines custom executor settings""" return self.settings_cls is not None @@ -84,7 +103,7 @@ def get_settings_info(self) -> List[Dict[str, Any]]: return [] else: - def fmt_default(thefield): + def fmt_default(thefield: Field) -> Any: if thefield.default is not MISSING: if callable(thefield.default): return "" @@ -110,12 +129,12 @@ def fmt_default(thefield): for thefield in fields(self.settings_cls) ] - def register_cli_args(self, argparser): + def register_cli_args(self, argparser: "ArgumentParser") -> None: """Add arguments derived from self.executor_settings to given argparser.""" # Cut out early if we don't have custom parameters to add - if not self.has_settings_cls(): + if self.settings_cls is None: return # Convenience handle @@ -169,8 +188,8 @@ def register_cli_args(self, argparser): settings.add_argument(*args, **kwargs) - def validate_settings(self, settings): - def get_description(thefield): + def validate_settings(self, settings: TSettingsBase) -> None: + def get_description(thefield: Field) -> str: envvar = ( f" (or environment variable {self.get_envvar(thefield.name)})" if thefield.metadata.get("env_var", None) @@ -192,30 +211,30 @@ def get_description(thefield): f"plugin {self.name}: {', '.join(cli_args)}." ) - def get_settings(self, args) -> Union[SettingsBase, TaggedSettings]: + def get_settings(self, args: Any) -> Union[TSettingsBase, TaggedSettings]: """Return an instance of self.executor_settings with values from args. This helper function will select executor plugin namespaces arguments for a dataclass. It allows us to pass them from the custom executor -> custom argument parser -> back into dataclass -> snakemake. """ - if not self.has_settings_cls(): + dc = self.settings_cls + if dc is None: if self.support_tagged_values: return TaggedSettings() else: - return SettingsBase() + return cast(TSettingsBase, SettingsBase()) # We will parse the args from snakemake back into the dataclass - dc = self.settings_cls - def get_name_and_value(field): + def get_name_and_value(field: Field) -> tuple[str, Any]: # This is the actual field name without the prefix prefixed_name = self._get_prefixed_name(thefield.name).replace("-", "_") value = getattr(args, prefixed_name) return field.name, value - kwargs_tagged = defaultdict(dict) - kwargs_all = dict() + kwargs_tagged: Dict[str | None, Dict[str, Any]] = defaultdict(dict) + kwargs_all: Dict[str, Any] = dict() required_args = set() field_names = dict() @@ -229,7 +248,9 @@ def get_name_and_value(field): if value is None: continue - def extract_values(value, thefield, name, tag=None): + def extract_values( + value: Any, thefield: Field, name: str, tag: Optional[str] = None + ) -> None: # This will only add instantiated values, and # skip over dataclasses._MISSING_TYPE and similar if isinstance(value, ArgTypes): @@ -247,7 +268,7 @@ def extract_values(value, thefield, name, tag=None): value = self._parse_type( thefield, value, thefield.metadata["type"] ) - elif thefield.type != str: + elif thefield.type != str and isinstance(thefield.type, type): value = self._parse_type(thefield, value, thefield.type) if tag is None: kwargs_all[name] = value @@ -297,8 +318,8 @@ def get_cli_arg(self, field_name: str) -> str: def _get_prefixed_name(self, field_name: str) -> str: return f"{self.cli_prefix}_{field_name}" - def _parse_type(self, thefield, value, thetype): - def apply_type(value, thetype): + def _parse_type(self, thefield: Field, value: Any, thetype: Type) -> Any: + def apply_type(value: Any, thetype: Type) -> Any: try: return thetype(value) except Exception as e: From ea664278f28353ad825b4a3fbf08960f920712f5 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Thu, 27 Mar 2025 09:53:24 -0400 Subject: [PATCH 7/8] refactor: enhance type hints in PluginRegistryBase and SettingsEnumBase for improved clarity and type safety --- .../plugin_registry/__init__.py | 26 ++++-- src/snakemake_interface_common/settings.py | 88 +++++++++++++++++-- 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/snakemake_interface_common/plugin_registry/__init__.py b/src/snakemake_interface_common/plugin_registry/__init__.py index a48caf8..55b5f33 100644 --- a/src/snakemake_interface_common/plugin_registry/__init__.py +++ b/src/snakemake_interface_common/plugin_registry/__init__.py @@ -7,24 +7,32 @@ import types import pkgutil import importlib -from typing import List, Mapping +from typing import List, Mapping, TYPE_CHECKING, TypeVar, Generic from snakemake_interface_common.exceptions import InvalidPluginException from snakemake_interface_common.plugin_registry.plugin import PluginBase from snakemake_interface_common.plugin_registry.attribute_types import AttributeType +if TYPE_CHECKING: + from argparse import ArgumentParser -class PluginRegistryBase(ABC): +TPlugin = TypeVar("TPlugin", bound=PluginBase, covariant=True) + + +class PluginRegistryBase(ABC, Generic[TPlugin]): """This class is a singleton that holds all registered executor plugins.""" _instance = None + plugins: dict[str, TPlugin] - def __new__(cls): + def __new__( + cls: type["PluginRegistryBase[TPlugin]"], + ) -> "PluginRegistryBase[TPlugin]": if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance - def __init__(self): + def __init__(self) -> None: if hasattr(self, "plugins"): # init has been called before return @@ -53,13 +61,13 @@ def get_plugin_package_name(self, plugin_name: str) -> str: """Get the package name of a plugin by name.""" return f"{self.module_prefix.replace('_', '-')}{plugin_name}" - def register_cli_args(self, argparser): + def register_cli_args(self, argparser: "ArgumentParser") -> None: """Add arguments derived from self.executor_settings to given argparser.""" for _, plugin in self.plugins.items(): plugin.register_cli_args(argparser) - def collect_plugins(self): + def collect_plugins(self) -> None: """Collect plugins and call register_plugin for each.""" self.plugins = dict() @@ -77,7 +85,7 @@ def collect_plugins(self): module = importlib.import_module(moduleinfo.name) self.register_plugin(moduleinfo.name, module) - def register_plugin(self, name: str, plugin: types.ModuleType): + def register_plugin(self, name: str, plugin: types.ModuleType) -> None: """Validate and register a plugin. Does nothing if the plugin is already registered. @@ -92,7 +100,7 @@ def register_plugin(self, name: str, plugin: types.ModuleType): self.plugins[plugin_name] = self.load_plugin(plugin_name, plugin) - def validate_plugin(self, name: str, module: types.ModuleType): + def validate_plugin(self, name: str, module: types.ModuleType) -> None: """Validate a plugin for attributes and naming""" expected_attributes = self.expected_attributes() for attr, attr_type in expected_attributes.items(): @@ -123,7 +131,7 @@ def validate_plugin(self, name: str, module: types.ModuleType): def module_prefix(self) -> str: ... @abstractmethod - def load_plugin(self, name: str, module: types.ModuleType) -> PluginBase: + def load_plugin(self, name: str, module: types.ModuleType) -> TPlugin: """Load a plugin by name.""" ... diff --git a/src/snakemake_interface_common/settings.py b/src/snakemake_interface_common/settings.py index e76ef9e..d96f5e3 100644 --- a/src/snakemake_interface_common/settings.py +++ b/src/snakemake_interface_common/settings.py @@ -1,43 +1,117 @@ from abc import abstractmethod from enum import Enum -from typing import FrozenSet, List, Set, TypeVar +from typing import Callable, FrozenSet, Iterable, List, Set, TypeVar, cast TSettingsEnumBase = TypeVar("TSettingsEnumBase", bound="SettingsEnumBase") +TContainer = TypeVar("TContainer") + class SettingsEnumBase(Enum): + """A base enumeration class for settings with utility methods for choice handling. + + Base enumeration class that extends Enum to provide common functionality for + settings enumerations, including methods to parse and format choices. + + Methods + ------- + choices() : List[str] + Returns a sorted list of all enum choices in lowercase with hyphens. + all() : FrozenSet[TSettingsEnumBase] + Returns a frozenset of all enum members except those marked to skip. + parse_choices_list(choices: List[str]) : List[TSettingsEnumBase] + Converts a list of string choices into enum members. + parse_choices_set(choices: List[str]) : Set[TSettingsEnumBase] + Converts a list of string choices into a set of enum members. + parse_choice(choice: str) : TSettingsEnumBase + Converts a single string choice into an enum member. + skip_for_all() : FrozenSet[TSettingsEnumBase] + Abstract method to define members to exclude from all(). + item_to_choice() : str + Converts an enum member name to lowercase with hyphens. + + Abstract Methods + ---------------- + skip_for_all() : FrozenSet[TSettingsEnumBase] + Abstract method to define members to exclude from all(). + + Notes + ----- + The class handles conversion between: + - Internal representation: UPPERCASE with underscores (e.g., "SOME_SETTING") + - External representation: lowercase with hyphens (e.g., "some-setting") + + Examples + -------- + >>> class MySettings(SettingsEnumBase): + ... SETTING_ONE = "value1" + ... SETTING_TWO = "value2" + ... + ... @classmethod + ... def skip_for_all(cls): + ... return frozenset([cls.SETTING_TWO]) + ... + >>> MySettings.choices() + ['setting-one', 'setting-two'] + >>> MySettings.all() + frozenset({}) + >>> MySettings.parse_choices_list(["setting-one", "setting-two"]) + [, ] + >>> MySettings.parse_choice("setting-one") + + >>> MySettings.SETTING_ONE.item_to_choice() + 'setting-one' + """ + @classmethod def choices(cls) -> List[str]: + """Returns a sorted list of all enum choices in lowercase with hyphens.""" return sorted(item.item_to_choice() for item in cls) @classmethod def all(cls) -> FrozenSet[TSettingsEnumBase]: - skip = cls.skip_for_all() - return frozenset(item for item in cls if item not in skip) + """Returns a frozenset of all enum members except those marked to skip.""" + skip: FrozenSet[TSettingsEnumBase] = cls.skip_for_all() + return frozenset( + cast(TSettingsEnumBase, item) for item in cls if item not in skip + ) @classmethod def parse_choices_list(cls, choices: List[str]) -> List[TSettingsEnumBase]: + """Converts a list of string choices into enum members.""" return cls._parse_choices_into(choices, list) @classmethod def parse_choices_set(cls, choices: List[str]) -> Set[TSettingsEnumBase]: + """Converts a list of string choices into a set of enum members.""" return cls._parse_choices_into(choices, set) @classmethod - def _parse_choices_into(cls, choices: str, container) -> List[TSettingsEnumBase]: - return container(cls.parse_choice(choice) for choice in choices) + def _parse_choices_into( + cls, + choices: List[str], + container: Callable[[Iterable[TSettingsEnumBase]], TContainer], + ) -> TContainer: + """Helper method to parse choices into a specified container type.""" + return container( + cast(TSettingsEnumBase, cls.parse_choice(choice)) for choice in choices + ) @classmethod - def parse_choice(cls, choice: str) -> TSettingsEnumBase: + def parse_choice(cls, choice: str) -> "SettingsEnumBase": + """Converts a single string choice into an enum member.""" return cls[choice.replace("-", "_").upper()] @classmethod @abstractmethod def skip_for_all(cls) -> FrozenSet[TSettingsEnumBase]: + """Abstract method to define members to exclude from all().""" return frozenset() def item_to_choice(self) -> str: + """Converts an enum member name to lowercase with hyphens.""" return self.name.replace("_", "-").lower() - def __str__(self): + def __str__(self) -> str: + """Returns the string representation of the enum member (lowercase with hyphens).""" return self.item_to_choice() From abd814d1b8bf5ca47fa99ea5137789fe6c58338e Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Thu, 27 Mar 2025 11:33:13 -0400 Subject: [PATCH 8/8] fix: correct variable name in get_name_and_value method for accurate field handling --- src/snakemake_interface_common/plugin_registry/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snakemake_interface_common/plugin_registry/plugin.py b/src/snakemake_interface_common/plugin_registry/plugin.py index 79d6008..10f727e 100644 --- a/src/snakemake_interface_common/plugin_registry/plugin.py +++ b/src/snakemake_interface_common/plugin_registry/plugin.py @@ -229,7 +229,7 @@ def get_settings(self, args: Any) -> Union[TSettingsBase, TaggedSettings]: def get_name_and_value(field: Field) -> tuple[str, Any]: # This is the actual field name without the prefix - prefixed_name = self._get_prefixed_name(thefield.name).replace("-", "_") + prefixed_name = self._get_prefixed_name(field.name).replace("-", "_") value = getattr(args, prefixed_name) return field.name, value