From 4ec29eb8fca96384de681aa564f7f73d4a6d294d Mon Sep 17 00:00:00 2001 From: bfs2021 <101915083+bfs2021@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:30:38 +0800 Subject: [PATCH] fix(python): ship a PEP 561 py.typed marker and make the stubs mypy-clean The wheels carry .pyi stubs for the whole package but no py.typed marker, so mypy ignored them and every ioh import degraded to Any. Add the empty marker (both the wheel's packages list and the sdist include pick it up from ioh/), and fix the errors the now-honoured stubs themselves report: - None defaults on parameters typed as str/Callable/list in wrap_problem, Experiment and add_custom_problem - any/callable used as types in Experiment.apply/add_custom_problem - the logger stub importing the property submodule under the name property, which shadowed the builtin decorator for every class-level @property in the file - __call__ overloads whose 1-D and 2-D variants were indistinguishable to mypy (shape is now expressed in the ndarray type parameter) - MetaData.__eq__ widened to the object supertype signature Fixes #237 --- ioh/__init__.pyi | 8 ++++---- ioh/iohcpp/__init__.pyi | 6 ++++-- ioh/iohcpp/logger/__init__.pyi | 23 +++++++++++++---------- ioh/iohcpp/problem/__init__.pyi | 8 ++++---- ioh/py.typed | 0 5 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 ioh/py.typed diff --git a/ioh/__init__.pyi b/ioh/__init__.pyi index 09f407e4a..5f8c3d290 100644 --- a/ioh/__init__.pyi +++ b/ioh/__init__.pyi @@ -28,7 +28,7 @@ class ProblemClass(enum.Enum): def problems(self): ... def get_problem(fid: int | str, instance: int = 1, dimension: int = 5, problem_class: ProblemClass = ...) -> ProblemType: ... -def wrap_problem(function: typing.Callable[[numpy.typing.NDArray], float], name: str = None, problem_class: ProblemClass = ..., dimension: int = 5, instance: int = 1, optimization_type: OptimizationType = ..., lb: VariableType = 0, ub: VariableType = 1, transform_variables: typing.Callable[[numpy.typing.NDArray, int], numpy.typing.NDArray | typing.Sequence[VariableType]] = None, transform_objectives: typing.Callable[[float, int], float] = None, calculate_objective: typing.Callable[[int, int], IntegerSolution | RealSolution] = None, constraints: list[IntegerConstraint | RealConstraint] = None) -> ProblemType: ... +def wrap_problem(function: typing.Callable[[numpy.typing.NDArray], float], name: str | None = None, problem_class: ProblemClass = ..., dimension: int = 5, instance: int = 1, optimization_type: OptimizationType = ..., lb: VariableType = 0, ub: VariableType = 1, transform_variables: typing.Callable[[numpy.typing.NDArray, int], numpy.typing.NDArray | typing.Sequence[VariableType]] | None = None, transform_objectives: typing.Callable[[float, int], float] | None = None, calculate_objective: typing.Callable[[int, int], IntegerSolution | RealSolution] | None = None, constraints: list[IntegerConstraint | RealConstraint] | None = None) -> ProblemType: ... def get_problem_id(problem_name: str, problem_class: ProblemClass) -> int: ... class Experiment: @@ -50,10 +50,10 @@ class Experiment: remove_data: Incomplete enforce_bounds: Incomplete old_logger: Incomplete - def __init__(self, algorithm: typing.Any, fids: list[int], iids: list[int], dims: list[int], reps: int = 1, problem_class: ProblemClass = ..., njobs: int = 1, logged: bool = True, logger_triggers: list[logger.trigger.Trigger] = ..., logger_additional_properties: list[logger.property.AbstractProperty] = [], output_directory: str = './', folder_name: str = 'ioh_data', algorithm_name: str = None, algorithm_info: str = 'algorithm_info', store_positions: bool = False, experiment_attributes: dict[str, str] = {}, run_attributes: list[str] = [], logged_attributes: list[str] = [], merge_output: bool = True, zip_output: bool = True, remove_data: bool = False, enforce_bounds: bool = False, old_logger: bool = False) -> None: ... + def __init__(self, algorithm: typing.Any, fids: list[int], iids: list[int], dims: list[int], reps: int = 1, problem_class: ProblemClass = ..., njobs: int = 1, logged: bool = True, logger_triggers: list[logger.trigger.Trigger] = ..., logger_additional_properties: list[logger.property.AbstractProperty] = [], output_directory: str = './', folder_name: str = 'ioh_data', algorithm_name: str | None = None, algorithm_info: str = 'algorithm_info', store_positions: bool = False, experiment_attributes: dict[str, str] = {}, run_attributes: list[str] = [], logged_attributes: list[str] = [], merge_output: bool = True, zip_output: bool = True, remove_data: bool = False, enforce_bounds: bool = False, old_logger: bool = False) -> None: ... def evaluate(self, ii: int, job: tuple[int, int, int]): ... - def apply(self, algorithm: any, problem: ProblemType): ... - def add_custom_problem(self, p: callable, name: str = None, **kwargs): ... + def apply(self, algorithm: typing.Any, problem: ProblemType): ... + def add_custom_problem(self, p: typing.Callable[..., typing.Any], name: str | None = None, **kwargs): ... def merge_output_to_single_folder(self, prefix: str, target_folder: str): ... def merge_tmp_folders(self) -> None: ... def run(self): ... diff --git a/ioh/iohcpp/__init__.pyi b/ioh/iohcpp/__init__.pyi index 1ecbee63c..9615a9ad2 100644 --- a/ioh/iohcpp/__init__.pyi +++ b/ioh/iohcpp/__init__.pyi @@ -705,9 +705,11 @@ class MetaData: def __repr__(self) -> str: ... - def __eq__(self, arg: MetaData, /) -> bool: ... + def __eq__(self, arg: object, /) -> bool: ... - __hash__: None = None + # nanobind stubgen emits the unhashable marker this way; mypy 2.x needs + # the ignore because object.__hash__ is typed as a callable. + __hash__ = None # type: ignore[assignment] class LogInfo: def __init__(self, evaluations: int, raw_y: float, raw_y_best: float, transformed_y: float, transformed_y_best: float, y: float, y_best: float, x: Sequence[float], violations: Sequence[float], penalties: Sequence[float], optimum: RealSolution, has_improved: bool = False) -> None: diff --git a/ioh/iohcpp/logger/__init__.pyi b/ioh/iohcpp/logger/__init__.pyi index 84aeff52e..d6d0c7173 100644 --- a/ioh/iohcpp/logger/__init__.pyi +++ b/ioh/iohcpp/logger/__init__.pyi @@ -12,10 +12,13 @@ from ioh.iohcpp.logger import ( eaf as eaf, eah as eah, old as old, - property as property, - trigger as trigger + trigger as trigger, ) +# Imported by its full path under a local alias: binding the submodule as +# `property` shadows the builtin decorator the class definitions below need. +import ioh.iohcpp.logger.property as logger_property + class Logger: """Base class for all loggers""" @@ -36,9 +39,9 @@ class Logger: class AbstractLogger(Logger): """Base class for loggers which track properties""" - def __init__(self, triggers: Sequence[trigger.Trigger] = [], properties: Sequence[property.AbstractProperty] = []) -> None: ... + def __init__(self, triggers: Sequence[trigger.Trigger] = [], properties: Sequence[logger_property.AbstractProperty] = []) -> None: ... - def watch(self, arg: property.AbstractProperty, /) -> None: ... + def watch(self, arg: logger_property.AbstractProperty, /) -> None: ... def attach_problem(self, problem: ioh.iohcpp.MetaData) -> None: """attach a problem (MetaData) to a logger""" @@ -55,7 +58,7 @@ class Combine(Logger): def append(self, arg: Logger, /) -> None: ... class FlatFile(AbstractLogger): - def __init__(self, triggers: Sequence[trigger.Trigger], properties: Sequence[property.AbstractProperty], filename: str = 'IOH.dat', output_directory: str | os.PathLike = './', separator: str = '\t', comment: str = '#', no_value: str = 'None', end_of_line: str = '\n', repeat_header: bool = False, store_positions: bool = False, common_header_titles: Sequence[str] = ...) -> None: + def __init__(self, triggers: Sequence[trigger.Trigger], properties: Sequence[logger_property.AbstractProperty], filename: str = 'IOH.dat', output_directory: str | os.PathLike = './', separator: str = '\t', comment: str = '#', no_value: str = 'None', end_of_line: str = '\n', repeat_header: bool = False, store_positions: bool = False, common_header_titles: Sequence[str] = ...) -> None: """ A logger which stores all tracked properties to a file. @@ -96,7 +99,7 @@ class FlatFile(AbstractLogger): """attach a problem (MetaData) to a logger""" @overload - def watch(self, arg: property.AbstractProperty, /) -> None: ... + def watch(self, arg: logger_property.AbstractProperty, /) -> None: ... @overload def watch(self, arg0: object, arg1: str, /) -> None: ... @@ -107,7 +110,7 @@ class FlatFile(AbstractLogger): def __repr__(self) -> str: ... class Store(AbstractLogger): - def __init__(self, triggers: Sequence[trigger.Trigger], properties: Sequence[property.AbstractProperty]) -> None: + def __init__(self, triggers: Sequence[trigger.Trigger], properties: Sequence[logger_property.AbstractProperty]) -> None: """ A logger which stores all tracked properties in memory. @@ -132,7 +135,7 @@ class Store(AbstractLogger): """attach a problem (MetaData) to a logger""" @overload - def watch(self, arg: property.AbstractProperty, /) -> None: ... + def watch(self, arg: logger_property.AbstractProperty, /) -> None: ... @overload def watch(self, arg0: object, arg1: str, /) -> None: @@ -145,7 +148,7 @@ class Store(AbstractLogger): def __repr__(self) -> str: ... class Analyzer(AbstractLogger): - def __init__(self, triggers: Sequence[trigger.Trigger] = ..., additional_properties: Sequence[property.AbstractProperty] = [], root: str | os.PathLike = ..., folder_name: str = 'ioh_data', algorithm_name: str = 'algorithm_name', algorithm_info: str = 'algorithm_info', store_positions: bool = False) -> None: + def __init__(self, triggers: Sequence[trigger.Trigger] = ..., additional_properties: Sequence[logger_property.AbstractProperty] = [], root: str | os.PathLike = ..., folder_name: str = 'ioh_data', algorithm_name: str = 'algorithm_name', algorithm_info: str = 'algorithm_info', store_positions: bool = False) -> None: """ A logger which stores all tracked properties to a file. @@ -195,7 +198,7 @@ class Analyzer(AbstractLogger): """attach a problem (MetaData) to a logger""" @overload - def watch(self, arg: property.AbstractProperty, /) -> None: ... + def watch(self, arg: logger_property.AbstractProperty, /) -> None: ... @overload def watch(self, arg0: object, arg1: str, /) -> None: ... diff --git a/ioh/iohcpp/problem/__init__.pyi b/ioh/iohcpp/problem/__init__.pyi index 5e7cb3a08..3ebd048d8 100644 --- a/ioh/iohcpp/problem/__init__.pyi +++ b/ioh/iohcpp/problem/__init__.pyi @@ -34,7 +34,7 @@ class RealSingleObjective: """Remove the specified logger from the problem.""" @overload - def __call__(self, arg: Annotated[NDArray[numpy.float64], dict(shape=(None,), order='C', device='cpu', writable=False)], /) -> float: ... + def __call__(self, arg: Annotated[numpy.ndarray[tuple[int], numpy.dtype[numpy.float64]], dict(order='C', device='cpu', writable=False)], /) -> float: ... @overload def __call__(self, arg: Sequence[float], /) -> float: @@ -52,7 +52,7 @@ class RealSingleObjective: """ @overload - def __call__(self, arg: Annotated[NDArray[numpy.float64], dict(shape=(None, None), order='C', device='cpu', writable=False)], /) -> Annotated[NDArray[numpy.float64], dict(shape=(None,))]: ... + def __call__(self, arg: Annotated[numpy.ndarray[tuple[int, int], numpy.dtype[numpy.float64]], dict(order='C', device='cpu', writable=False)], /) -> Annotated[NDArray[numpy.float64], dict(shape=(None,))]: ... @overload def __call__(self, arg: Sequence[Sequence[float]], /) -> Annotated[NDArray[numpy.float64], dict(shape=(None,))]: @@ -185,7 +185,7 @@ class IntegerSingleObjective: """Remove the specified logger from the problem.""" @overload - def __call__(self, arg: Annotated[NDArray, dict(shape=(None,), order='C', device='cpu')], /) -> float: ... + def __call__(self, arg: Annotated[numpy.ndarray[tuple[int], numpy.dtype[numpy.integer]], dict(order='C', device='cpu')], /) -> float: ... @overload def __call__(self, arg: Sequence[int], /) -> float: @@ -203,7 +203,7 @@ class IntegerSingleObjective: """ @overload - def __call__(self, arg: Annotated[NDArray, dict(shape=(None, None), order='C', device='cpu')], /) -> Annotated[NDArray[numpy.float64], dict(shape=(None,))]: ... + def __call__(self, arg: Annotated[numpy.ndarray[tuple[int, int], numpy.dtype[numpy.integer]], dict(order='C', device='cpu')], /) -> Annotated[NDArray[numpy.float64], dict(shape=(None,))]: ... @overload def __call__(self, arg: Sequence[Sequence[int]], /) -> Annotated[NDArray[numpy.float64], dict(shape=(None,))]: diff --git a/ioh/py.typed b/ioh/py.typed new file mode 100644 index 000000000..e69de29bb