From a57f2c82293b40828f4e4929c98c314ff51fd36c Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Tue, 22 Sep 2026 12:40:32 +0200 Subject: [PATCH 1/2] Remove Python 3.9 support --- .github/workflows/ci_python_compatibility.yaml | 2 +- pyproject.toml | 3 +-- tox.ini | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_python_compatibility.yaml b/.github/workflows/ci_python_compatibility.yaml index 5c0ef65..16cee0a 100644 --- a/.github/workflows/ci_python_compatibility.yaml +++ b/.github/workflows/ci_python_compatibility.yaml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Check out the source code diff --git a/pyproject.toml b/pyproject.toml index a4d58ee..c79f14f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,14 +18,13 @@ classifiers = [ "Intended Audience :: Developers", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] -requires-python = ">=3.9, <4" +requires-python = ">=3.10, <4" dependencies = [ "click>=6.5", "importlib-metadata; python_version<'3.10'", diff --git a/tox.ini b/tox.ini index 1037ce8..741b8d1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39, py310, py311, py312, py313, py314 +envlist = py310, py311, py312, py313, py314 skip_missing_interpreters = true [testenv] @@ -18,7 +18,6 @@ commands = [gh-actions] python = - 3.9: py39 3.10: py310 3.11: py311 3.12: py312 From 4af8dc2d1a650e5561ca3ae1a18dc9776322a03e Mon Sep 17 00:00:00 2001 From: Lourens Veen Date: Tue, 22 Sep 2026 12:41:09 +0200 Subject: [PATCH 2/2] Upgrade code to Python 3.10+ --- ymmsl/command_line.py | 10 ++--- ymmsl/conversion/convert_v0_1_to_v0_2.py | 10 ++--- ymmsl/io.py | 8 ++-- ymmsl/v0_1/checkpoint.py | 14 +++---- ymmsl/v0_1/component.py | 22 +++++----- ymmsl/v0_1/configuration.py | 50 ++++++++++------------- ymmsl/v0_1/execution.py | 22 +++++----- ymmsl/v0_1/identity.py | 20 ++++----- ymmsl/v0_1/model.py | 6 +-- ymmsl/v0_1/settings.py | 36 ++++++++-------- ymmsl/v0_2/component.py | 10 ++--- ymmsl/v0_2/configuration.py | 40 ++++++++---------- ymmsl/v0_2/implementation.py | 6 +-- ymmsl/v0_2/model.py | 14 +++---- ymmsl/v0_2/ports.py | 52 ++++++++++++------------ ymmsl/v0_2/program.py | 26 ++++++------ ymmsl/v0_2/resolver.py | 19 +++------ ymmsl/v0_2/supported_settings.py | 26 +++++------- ymmsl/v0_2/tests/test_imports.py | 4 +- ymmsl/v0_2/tests/test_ports.py | 2 +- ymmsl/v0_2/tests/test_resolver.py | 7 +--- ymmsl/v0_2/timeline_resolver.py | 6 ++- 22 files changed, 189 insertions(+), 221 deletions(-) diff --git a/ymmsl/command_line.py b/ymmsl/command_line.py index 5b7fe58..732e25c 100644 --- a/ymmsl/command_line.py +++ b/ymmsl/command_line.py @@ -1,7 +1,7 @@ import os import warnings from shutil import copyfile -from typing import Dict, Optional, TextIO, Type, Union +from typing import Dict, TextIO, Type import click @@ -13,12 +13,12 @@ def showwarning( - message: Union[Warning, str], + message: Warning | str, category: Type[Warning], filename: str, lineno: int, - file: Optional[TextIO] = None, - line: Optional[str] = None, + file: TextIO | None = None, + line: str | None = None, ) -> None: print(f"WARNING: {message}", file=file) @@ -68,7 +68,7 @@ def ymmsl() -> None: ), ) @click.option("-t", "--to", default="v0.2", help='Version to convert to, e.g. "v0.2".') -def convert(input_file: str, output_file: Optional[str], to: str) -> None: +def convert(input_file: str, output_file: str | None, to: str) -> None: """Convert a yMMSL file to a later version When upgrading in place, and/or if an output file is specified and it exists, a diff --git a/ymmsl/conversion/convert_v0_1_to_v0_2.py b/ymmsl/conversion/convert_v0_1_to_v0_2.py index 159cd3a..230a22a 100644 --- a/ymmsl/conversion/convert_v0_1_to_v0_2.py +++ b/ymmsl/conversion/convert_v0_1_to_v0_2.py @@ -1,6 +1,6 @@ import warnings from copy import deepcopy -from typing import Dict, List, MutableMapping, Optional +from typing import Dict, List, MutableMapping import ymmsl.v0_1 as v0_1 import ymmsl.v0_2 as v0_2 @@ -59,7 +59,7 @@ def convert_component(component: v0_1.Component) -> v0_2.Component: """Convert a v0.1 Component object to a v0.2 Component.""" ports = component.ports if component.ports else v0_1.Ports() description = "Please add a description" - implementation: Optional[str] = None + implementation: str | None = None if component.implementation is not None: implementation = str(component.implementation) @@ -153,8 +153,8 @@ def convert_implementation(impl: v0_1.Implementation) -> v0_2.Program: The corresponding program expressed in yMMSL v0.2. """ description = "Please add a description" - base_env: Optional[v0_1.BaseEnv] = impl.base_env - env: Optional[Dict[str, str]] = impl.env + base_env: v0_1.BaseEnv | None = impl.base_env + env: Dict[str, str] | None = impl.env execution_model = v0_2.ExecutionModel[impl.execution_model.name] @@ -201,7 +201,7 @@ def convert_ports(ports: v0_1.Ports) -> v0_2.Ports: def convert_resources( resources: MutableMapping[v0_1.Reference, v0_1.ResourceRequirements], - models: Optional[List[v0_2.Model]], + models: List[v0_2.Model] | None, ) -> MutableMapping[v0_2.Reference, v0_2.ResourceRequirements]: if not models: warnings.warn( diff --git a/ymmsl/io.py b/ymmsl/io.py index 5da03c8..75e87fa 100644 --- a/ymmsl/io.py +++ b/ymmsl/io.py @@ -1,7 +1,7 @@ """Loading and saving functions.""" from pathlib import Path -from typing import IO, Any, Type, TypeVar, Union +from typing import IO, Any, Type, TypeVar import yatiml @@ -74,7 +74,7 @@ _load = yatiml.load_function(*_classes) # type: ignore -def load(source: Union[str, Path, IO[Any]]) -> Document: +def load(source: str | Path | IO[Any]) -> Document: """Loads a yMMSL document from a string or a file. Args: @@ -94,7 +94,7 @@ def load(source: Union[str, Path, IO[Any]]) -> Document: T = TypeVar("T", bound=Document) -def load_as(as_type: Type[T], source: Union[str, Path, IO[Any]]) -> T: +def load_as(as_type: Type[T], source: str | Path | IO[Any]) -> T: """Loads and converts a yMMSL document from a string or a file. If the file is of a version older than the specified version, then it will be @@ -145,7 +145,7 @@ def dump(config: Document) -> str: _save = yatiml.dump_function(*_classes) -def save(config: Document, target: Union[str, Path, IO[Any]]) -> None: +def save(config: Document, target: str | Path | IO[Any]) -> None: """Saves a yMMSL configuration to a file. The `config` argument should be either a v0_1.PartialConfiguration, a diff --git a/ymmsl/v0_1/checkpoint.py b/ymmsl/v0_1/checkpoint.py index ea019ed..ac689b7 100644 --- a/ymmsl/v0_1/checkpoint.py +++ b/ymmsl/v0_1/checkpoint.py @@ -1,6 +1,6 @@ """Definitions for describing checkpoints.""" -from typing import List, Optional, Union +from typing import List import yaml import yatiml @@ -36,9 +36,9 @@ class CheckpointRangeRule(CheckpointRule): def __init__( self, - start: Optional[Union[float, int]] = None, - stop: Optional[Union[float, int]] = None, - every: Union[float, int] = 0, + start: float | int | None = None, + stop: float | int | None = None, + every: float | int = 0, ) -> None: """Create a checkpoint range. @@ -83,7 +83,7 @@ class CheckpointAtRule(CheckpointRule): at: List of checkpoints. """ - def __init__(self, at: Optional[List[Union[float, int]]]) -> None: + def __init__(self, at: List[float | int] | None) -> None: """Create checkpoint rules. Args: @@ -136,8 +136,8 @@ class Checkpoints: def __init__( self, at_end: bool = False, - wallclock_time: Optional[List[CheckpointRule]] = None, - simulation_time: Optional[List[CheckpointRule]] = None, + wallclock_time: List[CheckpointRule] | None = None, + simulation_time: List[CheckpointRule] | None = None, ) -> None: """Create checkpoint definitions. diff --git a/ymmsl/v0_1/component.py b/ymmsl/v0_1/component.py index 5e793a6..f731af7 100644 --- a/ymmsl/v0_1/component.py +++ b/ymmsl/v0_1/component.py @@ -7,8 +7,6 @@ Dict, # noqa: F401 Iterable, List, - Optional, - Union, ) import yaml @@ -90,10 +88,10 @@ class Ports: def __init__( self, - f_init: Union[None, str, List[str]] = None, - o_i: Union[None, str, List[str]] = None, - s: Union[None, str, List[str]] = None, - o_f: Union[None, str, List[str]] = None, + f_init: None | str | List[str] = None, + o_i: None | str | List[str] = None, + s: None | str | List[str] = None, + o_f: None | str | List[str] = None, ) -> None: """Create a Ports declaration. @@ -104,7 +102,7 @@ def __init__( o_f: The ports associated with the O_F operator """ - def to_list(ports: Union[None, str, List[str]]) -> List[Identifier]: + def to_list(ports: None | str | List[str]) -> List[Identifier]: if ports is None: return list() @@ -165,7 +163,7 @@ def operator(self, port_name: Identifier) -> Operator: raise KeyError(f'No port named "{port_name}" was found') - _yatiml_defaults: dict[str, Optional[list[str]]] = { + _yatiml_defaults: dict[str, list[str] | None] = { "f_init": [], "o_i": [], "s": [], @@ -200,9 +198,9 @@ class Component: def __init__( self, name: str, - implementation: Optional[str] = None, - multiplicity: Union[None, int, List[int]] = None, - ports: Optional[Ports] = None, + implementation: str | None = None, + multiplicity: None | int | List[int] = None, + ports: Ports | None = None, ) -> None: """Create a Component. @@ -219,7 +217,7 @@ def __init__( """ self.name = Reference(name) if implementation is None: - self.implementation: Optional[Reference] = None + self.implementation: Reference | None = None else: self.implementation = Reference(implementation) for part in self.implementation: diff --git a/ymmsl/v0_1/configuration.py b/ymmsl/v0_1/configuration.py index c2a2ab1..b0817b0 100644 --- a/ymmsl/v0_1/configuration.py +++ b/ymmsl/v0_1/configuration.py @@ -4,7 +4,7 @@ import logging from collections import OrderedDict from pathlib import Path -from typing import Dict, List, MutableMapping, Optional, Sequence, Union, cast +from typing import Dict, List, MutableMapping, Sequence, cast import yaml import yatiml @@ -50,20 +50,17 @@ class PartialConfiguration(Document): def __init__( self, - model: Optional[ModelReference] = None, - settings: Optional[Settings] = None, - implementations: Optional[ - Union[List[Implementation], Dict[Reference, Implementation]] - ] = None, - resources: Optional[ - Union[ - Sequence[ResourceRequirements], - MutableMapping[Reference, ResourceRequirements], - ] - ] = None, - description: Optional[str] = None, - checkpoints: Optional[Checkpoints] = None, - resume: Optional[Dict[Reference, Path]] = None, + model: ModelReference | None = None, + settings: Settings | None = None, + implementations: List[Implementation] + | Dict[Reference, Implementation] + | None = None, + resources: Sequence[ResourceRequirements] + | MutableMapping[Reference, ResourceRequirements] + | None = None, + description: str | None = None, + checkpoints: Checkpoints | None = None, + resume: Dict[Reference, Path] | None = None, ) -> None: """Create a Configuration. @@ -271,19 +268,16 @@ class Configuration(PartialConfiguration): def __init__( self, model: Model, - settings: Optional[Settings] = None, - implementations: Optional[ - Union[List[Implementation], Dict[Reference, Implementation]] - ] = None, - resources: Optional[ - Union[ - Sequence[ResourceRequirements], - MutableMapping[Reference, ResourceRequirements], - ] - ] = None, - description: Optional[str] = None, - checkpoints: Optional[Checkpoints] = None, - resume: Optional[Dict[Reference, Path]] = None, + settings: Settings | None = None, + implementations: List[Implementation] + | Dict[Reference, Implementation] + | None = None, + resources: Sequence[ResourceRequirements] + | MutableMapping[Reference, ResourceRequirements] + | None = None, + description: str | None = None, + checkpoints: Checkpoints | None = None, + resume: Dict[Reference, Path] | None = None, ) -> None: """Create a Configuration. diff --git a/ymmsl/v0_1/execution.py b/ymmsl/v0_1/execution.py index 418b377..e8ae3b2 100644 --- a/ymmsl/v0_1/execution.py +++ b/ymmsl/v0_1/execution.py @@ -2,7 +2,7 @@ from enum import Enum from pathlib import Path -from typing import Dict, List, Optional, Union, cast +from typing import Dict, List, cast import yaml import yatiml @@ -185,14 +185,14 @@ class Implementation: def __init__( self, name: Reference, - base_env: Optional[BaseEnv] = None, - modules: Union[str, List[str], None] = None, - virtual_env: Optional[Path] = None, - env: Optional[Dict[str, str]] = None, + base_env: BaseEnv | None = None, + modules: str | List[str] | None = None, + virtual_env: Path | None = None, + env: Dict[str, str] | None = None, execution_model: ExecutionModel = ExecutionModel.DIRECT, - executable: Optional[Path] = None, - args: Union[str, List[str], None] = None, - script: Union[str, List[str], None] = None, + executable: Path | None = None, + args: str | List[str] | None = None, + script: str | List[str] | None = None, can_share_resources: bool = True, keeps_state_for_next_use: KeepsStateForNextUse = KeepsStateForNextUse.NECESSARY, ) -> None: @@ -259,14 +259,14 @@ def __init__( self.name = name if isinstance(script, list): - self.script: Optional[str] = "\n".join(script) + "\n" + self.script: str | None = "\n".join(script) + "\n" else: self.script = script self.base_env = base_env if base_env else BaseEnv.MANAGER if isinstance(modules, str): - self.modules: Optional[list[str]] = modules.split(" ") + self.modules: list[str] | None = modules.split(" ") else: self.modules = modules self.virtual_env = virtual_env @@ -277,7 +277,7 @@ def __init__( self.executable = executable if isinstance(args, str): - self.args: Optional[list[str]] = [args] + self.args: list[str] | None = [args] else: self.args = args diff --git a/ymmsl/v0_1/identity.py b/ymmsl/v0_1/identity.py index f50fe28..b9120ca 100644 --- a/ymmsl/v0_1/identity.py +++ b/ymmsl/v0_1/identity.py @@ -3,7 +3,7 @@ import re from collections import UserString from copy import copy -from typing import Any, Generator, Iterable, List, Union, overload +from typing import Any, Generator, Iterable, List, overload import yatiml @@ -11,7 +11,7 @@ class Identifier(UserString): """A custom string type that represents an identifier. - An identifier may consist of upper- and lowercase characters, digits, and \ + An identifier may consist of upper- and lowercase characters, digits, and underscores. """ @@ -37,7 +37,7 @@ def __init__(self, seq: Any) -> None: ) -ReferencePart = Union[Identifier, int] +ReferencePart = Identifier | int class Reference(yatiml.String): @@ -49,9 +49,9 @@ class Reference(yatiml.String): - a Reference followed by a period and an Identifier, or - a Reference followed by an integer enclosed in square brackets. - In object form, they consist of a list of Identifiers and ints. The \ - first list item is always an Identifier. For the rest of the list, \ - an Identifier represents a period operator with that argument, \ + In object form, they consist of a list of Identifiers and ints. The + first list item is always an Identifier. For the rest of the list, + an Identifier represents a period operator with that argument, while an int represents the indexing operator with that argument. Reference objects act like a list of Identifiers and ints, you can @@ -68,7 +68,7 @@ class Reference(yatiml.String): modified, this will get your dictionary in a very confused state. """ - def __init__(self, parts: Union[str, List[ReferencePart]]) -> None: + def __init__(self, parts: str | List[ReferencePart]) -> None: """Create a Reference. Creates a Reference from either a string, which will be parsed, @@ -186,7 +186,7 @@ def __getitem__(self, key: int) -> ReferencePart: ... @overload def __getitem__(self, key: slice) -> "Reference": ... - def __getitem__(self, key: Union[int, slice]) -> Union["Reference", ReferencePart]: + def __getitem__(self, key: int | slice) -> "Reference | ReferencePart": """Get a part or a slice. If passed an int, e.g. ref[2], will return that part as an int @@ -210,7 +210,7 @@ def __getitem__(self, key: Union[int, slice]) -> Union["Reference", ReferencePar return Reference(self._parts[key]) raise ValueError("Subscript must be either an int or a slice") - def __setitem__(self, key: Union[int, slice], value: Any) -> None: + def __setitem__(self, key: int | slice, value: Any) -> None: """Does not set the value of a part. References are immutable, so they should not be modified, and @@ -225,7 +225,7 @@ def __setitem__(self, key: Union[int, slice], value: Any) -> None: ) def __add__( - self, other: Union["Reference", Iterable[ReferencePart], ReferencePart] + self, other: "Reference | Iterable[ReferencePart] | ReferencePart" ) -> "Reference": """Concatenates something onto a Reference. diff --git a/ymmsl/v0_1/model.py b/ymmsl/v0_1/model.py index c1d5b80..81f333d 100644 --- a/ymmsl/v0_1/model.py +++ b/ymmsl/v0_1/model.py @@ -5,9 +5,7 @@ Any, Dict, # noqa List, - Optional, Sequence, - Union, cast, ) @@ -190,7 +188,7 @@ def as_conduits(self) -> List[Conduit]: return self._conduits -AnyConduit = Union[Conduit, MulticastConduit] +AnyConduit = Conduit | MulticastConduit class ModelReference: @@ -235,7 +233,7 @@ def __init__( self, name: str, components: List[Component], - conduits: Optional[Sequence[AnyConduit]] = None, + conduits: Sequence[AnyConduit] | None = None, ) -> None: """Create a Model. diff --git a/ymmsl/v0_1/settings.py b/ymmsl/v0_1/settings.py index cf94f39..e0d10d1 100644 --- a/ymmsl/v0_1/settings.py +++ b/ymmsl/v0_1/settings.py @@ -3,22 +3,22 @@ from collections import OrderedDict from collections.abc import MutableMapping from copy import deepcopy -from typing import Any, Dict, Iterator, List, Optional, Tuple, TypeVar, Union, overload +from typing import Any, Dict, Iterator, List, Tuple, TypeAlias, TypeVar, overload import yatiml from ymmsl.v0_1.identity import Reference -SettingValue = Union[ - str, - int, - float, - bool, - List[int], - List[float], - List[List[float]], - yatiml.bool_union_fix, -] +SettingValue: TypeAlias = ( + str + | int + | float + | bool + | List[int] + | List[float] + | List[List[float]] + | yatiml.bool_union_fix +) _T = TypeVar("_T") @@ -31,7 +31,7 @@ class Settings(MutableMapping): for the submodel scales, model parameters and any other configuration. """ - def __init__(self, settings: Optional[Dict[str, SettingValue]] = None) -> None: + def __init__(self, settings: Dict[str, SettingValue] | None = None) -> None: """Create a Settings object. This will make a deep copy of the settings argument, if @@ -68,19 +68,19 @@ def __contains__(self, key: object) -> bool: return False return key in self._store - def __getitem__(self, key: Union[str, Reference]) -> SettingValue: + def __getitem__(self, key: str | Reference) -> SettingValue: """Returns an item, implements settings[name].""" if isinstance(key, str): key = Reference(key) return self._store[key] - def __setitem__(self, key: Union[str, Reference], value: SettingValue) -> None: + def __setitem__(self, key: str | Reference, value: SettingValue) -> None: """Sets a value, implements settings[name] = value.""" if isinstance(key, str): key = Reference(key) self._store[key] = value - def __delitem__(self, key: Union[str, Reference]) -> None: + def __delitem__(self, key: str | Reference) -> None: """Deletes a value, implements del(settings[name]).""" if isinstance(key, str): key = Reference(key) @@ -95,12 +95,12 @@ def __len__(self) -> int: return len(self._store) @overload - def get(self, key: Any, /) -> Union[Any, None]: ... + def get(self, key: Any, /) -> Any | None: ... @overload - def get(self, key: Any, /, default: _T) -> Union[Any, _T]: ... + def get(self, key: Any, /, default: _T) -> Any | _T: ... - def get(self, key: Any, /, default: Union[_T, None] = None) -> Union[Any, _T]: + def get(self, key: Any, /, default: _T | None = None) -> Any | _T: """Return the given setting, or default if it is not set. If default is not given, returns None. diff --git a/ymmsl/v0_2/component.py b/ymmsl/v0_2/component.py index c731fd8..e4a583b 100644 --- a/ymmsl/v0_2/component.py +++ b/ymmsl/v0_2/component.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union, cast +from typing import List, cast import yaml import yatiml @@ -40,9 +40,9 @@ def __init__( name: str, ports: Ports, description: str, - implementation: Optional[str] = None, + implementation: str | None = None, optional: bool = False, - multiplicity: Union[None, int, List[int]] = None, + multiplicity: None | int | List[int] = None, ) -> None: """Create a Component @@ -59,10 +59,10 @@ def __init__( self.ports = ports self.description = description self.optional = optional - self.timeline: Optional[Timeline] = None + self.timeline: Timeline | None = None if implementation is not None: - self.implementation: Optional[Reference] = Reference(implementation) + self.implementation: Reference | None = Reference(implementation) for part in self.implementation: if isinstance(part, int): raise ValueError( diff --git a/ymmsl/v0_2/configuration.py b/ymmsl/v0_2/configuration.py index 5542df7..feae2f9 100644 --- a/ymmsl/v0_2/configuration.py +++ b/ymmsl/v0_2/configuration.py @@ -3,7 +3,7 @@ import logging from copy import copy from pathlib import Path -from typing import Dict, List, MutableMapping, Optional, Sequence, Tuple, Union, cast +from typing import Dict, List, MutableMapping, Sequence, Tuple, cast import yaml import yatiml @@ -51,25 +51,17 @@ class Configuration(Document): def __init__( self, description: str = "", - imports: Optional[Sequence[ImportStatement]] = None, - models: Optional[ - Union[Sequence[Model], MutableMapping[Reference, Model]] - ] = None, - custom_implementations: Optional[ - MutableMapping[Reference, Optional[Reference]] - ] = None, - settings: Optional[Settings] = None, - programs: Optional[ - Union[Sequence[Program], MutableMapping[Reference, Program]] - ] = None, - resources: Optional[ - Union[ - Sequence[ResourceRequirements], - MutableMapping[Reference, ResourceRequirements], - ] - ] = None, - checkpoints: Optional[Checkpoints] = None, - resume: Optional[Dict[Reference, Path]] = None, + imports: Sequence[ImportStatement] | None = None, + models: Sequence[Model] | MutableMapping[Reference, Model] | None = None, + custom_implementations: MutableMapping[Reference, Reference | None] + | None = None, + settings: Settings | None = None, + programs: Sequence[Program] | MutableMapping[Reference, Program] | None = None, + resources: Sequence[ResourceRequirements] + | MutableMapping[Reference, ResourceRequirements] + | None = None, + checkpoints: Checkpoints | None = None, + resume: Dict[Reference, Path] | None = None, ) -> None: """Create a Configuration. @@ -116,7 +108,7 @@ def check_duplicate_impl_names( else: self.models = models - _CIType = MutableMapping[Reference, Optional[Reference]] # noqa: F841 + _CIType = MutableMapping[Reference, Reference | None] # noqa: F841 if custom_implementations is None: self.custom_implementations: _CIType = {} @@ -201,7 +193,7 @@ def update(self, overlay: "Configuration") -> None: self.resume.update(overlay.resume) def check_consistent( - self, check_runnable: bool = True, selected_model: Optional[str] = None + self, check_runnable: bool = True, selected_model: str | None = None ) -> None: """Checks that the configuration is internally consistent. @@ -271,7 +263,7 @@ def get_resources(self, name: Reference) -> ResourceRequirements: res_req = ThreadedResReq(name, 1) return res_req - def root_model(self, selected_model: Optional[Reference] = None) -> Model: + def root_model(self, selected_model: Reference | None = None) -> Model: """Return the root model of this configuration. If there are multiple models that are not used as an implementation in any @@ -598,7 +590,7 @@ def _setting_type_matches(self, value: SettingValue, typ: SettingType) -> bool: return False def _check_resources( - self, component_paths: Dict[Reference, Component], selected_model: Optional[str] + self, component_paths: Dict[Reference, Component], selected_model: str | None ) -> List[str]: """Check that each component path has a corresponding resource request. diff --git a/ymmsl/v0_2/implementation.py b/ymmsl/v0_2/implementation.py index f41eddc..ecabd8b 100644 --- a/ymmsl/v0_2/implementation.py +++ b/ymmsl/v0_2/implementation.py @@ -1,4 +1,4 @@ -from typing import Optional, cast +from typing import cast import yaml import yatiml @@ -25,9 +25,9 @@ class Implementation: def __init__( self, name: str, - ports: Optional[Ports] = None, + ports: Ports | None = None, description: str = "Please add a description!", - supported_settings: Optional[SupportedSettings] = None, + supported_settings: SupportedSettings | None = None, ) -> None: """Create an Implementation diff --git a/ymmsl/v0_2/model.py b/ymmsl/v0_2/model.py index 51350bc..86e8394 100644 --- a/ymmsl/v0_2/model.py +++ b/ymmsl/v0_2/model.py @@ -1,7 +1,7 @@ from collections import OrderedDict from copy import copy from enum import Enum -from typing import Any, List, Optional, Sequence, Union, cast +from typing import Any, List, Sequence, TypeAlias, cast import yatiml @@ -88,7 +88,7 @@ def __init__( self, sender: str, receiver: str, - filters: Optional[Union[str, List[ConduitFilter]]] = None, + filters: str | List[ConduitFilter] | None = None, ) -> None: """Create a Conduit. @@ -264,7 +264,7 @@ def as_conduits(self) -> List[Conduit]: return [Conduit(self.sender, recv_str) for recv_str in self.receiver] -AnyConduit = Union[Conduit, MulticastConduit] +AnyConduit: TypeAlias = Conduit | MulticastConduit class Model(Implementation): @@ -289,11 +289,11 @@ class Model(Implementation): def __init__( self, name: str, - ports: Optional[Ports] = None, + ports: Ports | None = None, description: str = "", - supported_settings: Optional[SupportedSettings] = None, - components: Optional[Sequence[Component]] = None, - conduits: Optional[Sequence[AnyConduit]] = None, + supported_settings: SupportedSettings | None = None, + components: Sequence[Component] | None = None, + conduits: Sequence[AnyConduit] | None = None, ) -> None: """Create a Model. diff --git a/ymmsl/v0_2/ports.py b/ymmsl/v0_2/ports.py index d479ace..e690179 100644 --- a/ymmsl/v0_2/ports.py +++ b/ymmsl/v0_2/ports.py @@ -1,5 +1,5 @@ from collections import OrderedDict -from typing import Any, Iterator, List, Optional, Sequence, Union, cast, overload +from typing import Any, Iterator, List, Sequence, cast, overload from ymmsl.v0_1.component import Operator # also the v0.2 version, import from here from ymmsl.v0_2.identity import Identifier, Reference @@ -42,12 +42,12 @@ def __init__(self, timeline: str) -> None: ... @overload def __init__( - self, timeline: Sequence[Union[str, Reference]], absolute: bool = True + self, timeline: Sequence[str | Reference], absolute: bool = True ) -> None: ... def __init__( self, - timeline: Union[str, Sequence[Union[str, Reference]]], + timeline: str | Sequence[str | Reference], absolute: bool = True, ) -> None: """Create a Timeline. @@ -56,13 +56,13 @@ def __init__( that are each a string that is also a valid Reference. """ - def make_new_reference(x: Union[str, Reference]) -> Reference: + def make_new_reference(x: str | Reference) -> Reference: return Reference(str(x)) if isinstance(timeline, str): if timeline == "": self.absolute = False - parts: Sequence[Union[str, Reference]] = [] + parts: Sequence[str | Reference] = [] elif timeline == ":": self.absolute = True parts = [] @@ -156,7 +156,7 @@ class Port: """ def __init__( - self, name: Identifier, operator: Operator, timeline: Optional[Timeline] = None + self, name: Identifier, operator: Operator, timeline: Timeline | None = None ) -> None: """Create a Port. @@ -184,13 +184,13 @@ def __eq__(self, other: Any) -> bool: return NotImplemented -_PortsSubAttrs = OrderedDict[str, Union[str, List[str]]] +_PortsSubAttrs = OrderedDict[str, str | List[str]] -_PortsAttrs = OrderedDict[str, Union[str, List[str], _PortsSubAttrs]] +_PortsAttrs = OrderedDict[str, str | List[str] | _PortsSubAttrs] -def _ensure_identifier(port_name: Union[str, Identifier]) -> Identifier: +def _ensure_identifier(port_name: str | Identifier) -> Identifier: if isinstance(port_name, str): port_name = Identifier(port_name) return port_name @@ -236,10 +236,10 @@ class Ports: @overload def __init__( self, - f_init: Union[None, str, List[str]] = None, - o_i: Union[None, str, List[str]] = None, - s: Union[None, str, List[str]] = None, - o_f: Union[None, str, List[str]] = None, + f_init: None | str | List[str] = None, + o_i: None | str | List[str] = None, + s: None | str | List[str] = None, + o_f: None | str | List[str] = None, ) -> None: ... @overload @@ -247,10 +247,10 @@ def __init__(self, f_init: List[Port]) -> None: ... def __init__( self, - f_init: Union[None, str, List[str], List[Port]] = None, - o_i: Union[None, str, List[str]] = None, - s: Union[None, str, List[str]] = None, - o_f: Union[None, str, List[str]] = None, + f_init: None | str | List[str] | List[Port] = None, + o_i: None | str | List[str] = None, + s: None | str | List[str] = None, + o_f: None | str | List[str] = None, ) -> None: """Create a Ports declaration. @@ -287,7 +287,7 @@ def __init__( else: self._ports = dict() - self._add_ports(Operator.F_INIT, cast(Union[None, str, List[str]], f_init)) + self._add_ports(Operator.F_INIT, cast(None | str | List[str], f_init)) self._add_ports(Operator.O_I, o_i) self._add_ports(Operator.S, s) self._add_ports(Operator.O_F, o_f) @@ -295,13 +295,13 @@ def __init__( def __len__(self) -> int: return len(self._ports) - def __contains__(self, port_name: Union[str, Identifier]) -> bool: + def __contains__(self, port_name: str | Identifier) -> bool: return _ensure_identifier(port_name) in self._ports - def __getitem__(self, port_name: Union[str, Identifier]) -> Port: + def __getitem__(self, port_name: str | Identifier) -> Port: return self._ports[_ensure_identifier(port_name)] - def __setitem__(self, port_name: Union[str, Identifier], port: Port) -> None: + def __setitem__(self, port_name: str | Identifier, port: Port) -> None: self._ports[_ensure_identifier(port_name)] = port def __iter__(self) -> Iterator[Identifier]: @@ -340,7 +340,7 @@ def receiving_port_names(self) -> List[Identifier]: ] def _add_ports( - self, op: Operator, ports: Union[None, str, List[str]], timeline: str = "" + self, op: Operator, ports: None | str | List[str], timeline: str = "" ) -> None: """Add the described ports to self._ports, helper function""" if ports is None: @@ -370,10 +370,10 @@ def _add_ports( def _yatiml_init( self, _yatiml_extra: OrderedDict, - f_init: Union[None, str, List[str]] = None, - o_i: Union[None, str, List[str]] = None, - s: Union[None, str, List[str]] = None, - o_f: Union[None, str, List[str]] = None, + f_init: None | str | List[str] = None, + o_i: None | str | List[str] = None, + s: None | str | List[str] = None, + o_f: None | str | List[str] = None, ) -> None: """Alternative initialisation when loading from YAML.""" self._ports = dict() diff --git a/ymmsl/v0_2/program.py b/ymmsl/v0_2/program.py index 33f498c..401e257 100644 --- a/ymmsl/v0_2/program.py +++ b/ymmsl/v0_2/program.py @@ -1,7 +1,7 @@ """Definitions for how to start programs.""" from pathlib import Path -from typing import Dict, List, Optional, Union, cast +from typing import Dict, List, cast import yaml import yatiml @@ -67,17 +67,17 @@ class Program(Implementation): def __init__( self, name: str, - ports: Optional[Ports] = None, + ports: Ports | None = None, description: str = "", - supported_settings: Optional[SupportedSettings] = None, - base_env: Optional[BaseEnv] = None, - modules: Union[str, List[str], None] = None, - virtual_env: Optional[Path] = None, - env: Optional[Dict[str, str]] = None, + supported_settings: SupportedSettings | None = None, + base_env: BaseEnv | None = None, + modules: str | List[str] | None = None, + virtual_env: Path | None = None, + env: Dict[str, str] | None = None, execution_model: ExecutionModel = ExecutionModel.DIRECT, - executable: Optional[Path] = None, - args: Union[str, List[str], None] = None, - script: Union[str, List[str], None] = None, + executable: Path | None = None, + args: str | List[str] | None = None, + script: str | List[str] | None = None, can_share_resources: bool = True, keeps_state_for_next_use: KeepsStateForNextUse = KeepsStateForNextUse.NECESSARY, ) -> None: @@ -147,7 +147,7 @@ def __init__( self.base_env = base_env if base_env else BaseEnv.MANAGER if isinstance(modules, str): - self.modules: Optional[List[str]] = modules.split(" ") + self.modules: List[str] | None = modules.split(" ") else: self.modules = modules @@ -161,12 +161,12 @@ def __init__( self.executable = executable if isinstance(args, str): - self.args: Optional[List[str]] = [args] + self.args: List[str] | None = [args] else: self.args = args if isinstance(script, list): - self.script: Optional[str] = "\n".join(script) + "\n" + self.script: str | None = "\n".join(script) + "\n" else: self.script = script diff --git a/ymmsl/v0_2/resolver.py b/ymmsl/v0_2/resolver.py index 475f4ed..ef92654 100644 --- a/ymmsl/v0_2/resolver.py +++ b/ymmsl/v0_2/resolver.py @@ -1,12 +1,12 @@ import logging import os -import sys from collections.abc import MutableMapping from copy import copy from difflib import get_close_matches +from importlib.metadata import EntryPoint, entry_points from pathlib import Path from textwrap import indent -from typing import Dict, List, Optional, Set, Tuple, TypeVar, Union +from typing import Dict, List, Set, Tuple, TypeAlias, TypeVar from yatiml import RecognitionError @@ -18,15 +18,10 @@ from ymmsl.v0_2.model import Model from ymmsl.v0_2.program import Program -if sys.version_info < (3, 10): - from importlib_metadata import EntryPoint, entry_points -else: - from importlib.metadata import EntryPoint, entry_points - _logger = logging.getLogger(__name__) -ModuleSource = Union[Path, EntryPoint] +ModuleSource: TypeAlias = Path | EntryPoint """Source file (Path) or EntryPoint for a yMMSL module""" @@ -246,9 +241,7 @@ def apply_custom_implementations( ylocals: Map from local to global names """ - def impl_hint_msg( - unknown: Reference, known_impls: Optional[List[str]] = None - ) -> str: + def impl_hint_msg(unknown: Reference, known_impls: List[str] | None = None) -> str: if known_impls is None: known_impls = [str(k) for k in ylocals.keys()] matches = get_close_matches(str(unknown), known_impls) @@ -516,7 +509,7 @@ def find_impl( def _load_from_entrypoints( module: Reference, -) -> Optional[Tuple[Configuration, EntryPoint]]: +) -> Tuple[Configuration, EntryPoint] | None: # Find entry point entrypoints = entry_points(group="ymmsl.module", name=str(module)) if not entrypoints: @@ -550,7 +543,7 @@ def _load_from_entrypoints( def _load_from_ymmsl_path( module_path: Path, ymmsl_path: list[Path] -) -> Optional[Tuple[Configuration, Path]]: +) -> Tuple[Configuration, Path] | None: for yp in ymmsl_path: try: loaded_file = yp / module_path diff --git a/ymmsl/v0_2/supported_settings.py b/ymmsl/v0_2/supported_settings.py index 01cf7cd..c7e1b6f 100644 --- a/ymmsl/v0_2/supported_settings.py +++ b/ymmsl/v0_2/supported_settings.py @@ -1,6 +1,6 @@ from collections.abc import MutableMapping from enum import Enum -from typing import Any, Dict, Iterator, List, Mapping, Optional, Tuple, Union, cast +from typing import Any, Dict, Iterator, List, Mapping, Tuple, cast import yaml import yatiml @@ -93,8 +93,8 @@ class SupportedSetting: def __init__( self, - name: Union[str, Identifier], - typ: Union[str, SettingType], + name: str | Identifier, + typ: str | SettingType, description: str, ) -> None: """Create a SupportedSetting. @@ -145,8 +145,8 @@ def _yatiml_savorize(cls, node: yatiml.Node) -> None: def _yatiml_init( self, name: Identifier, - typ: Optional[SettingType] = None, - description: Optional[Union[str, List[str], List[List[str]]]] = None, + typ: SettingType | None = None, + description: str | List[str] | List[List[str]] | None = None, ) -> None: """ Yeah, sorry. I wanted nice syntax for the users and couldn't find a cleaner @@ -190,7 +190,7 @@ def _yatiml_init( """ def list_to_setting_type( - description: Union[List[str], List[List[str]]], + description: List[str] | List[List[str]], ) -> SettingType: """convert ['something'] or [['something']] to a SettingType""" if len(description) == 0: @@ -312,9 +312,7 @@ class SupportedSettings(MutableMapping): def __init__( self, - supported_settings: Union[ - Mapping[str, str], List[SupportedSetting], None - ] = None, + supported_settings: Mapping[str, str] | List[SupportedSetting] | None = None, ) -> None: """Create a SupportedSettings object. @@ -350,15 +348,13 @@ def __str__(self) -> str: """Represent as a string, omitting the descriptions.""" return ", ".join([f"{s.name}: {s.typ}" for s in self._store.values()]) - def __getitem__(self, key: Union[str, Identifier]) -> SupportedSetting: + def __getitem__(self, key: str | Identifier) -> SupportedSetting: """Returns a supported setting, implements supported_settings[name].""" if isinstance(key, str): key = Identifier(key) return self._store[key] - def __setitem__( - self, key: Union[str, Identifier], value: Union[str, SupportedSetting] - ) -> None: + def __setitem__(self, key: str | Identifier, value: str | SupportedSetting) -> None: """Sets a value, implements supported_settings[name] = typ, desc.""" if isinstance(key, str): key = Identifier(key) @@ -366,7 +362,7 @@ def __setitem__( value = self._to_supported_setting(key, value) self._store[key] = value - def __delitem__(self, key: Union[str, Identifier]) -> None: + def __delitem__(self, key: str | Identifier) -> None: """Deletes a value, implements del(supported_settings[name]).""" if isinstance(key, str): key = Identifier(key) @@ -411,7 +407,7 @@ def _yatiml_savorize(cls, node: yatiml.Node) -> None: node.map_attribute_to_seq("supported_settings", "name", "description") def _yatiml_init( - self, supported_settings: Optional[List[SupportedSetting]] = None + self, supported_settings: List[SupportedSetting] | None = None ) -> None: # Take that list of supported settings and initialise the object SupportedSettings.__init__(self, supported_settings) diff --git a/ymmsl/v0_2/tests/test_imports.py b/ymmsl/v0_2/tests/test_imports.py index 270d267..2a63111 100644 --- a/ymmsl/v0_2/tests/test_imports.py +++ b/ymmsl/v0_2/tests/test_imports.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import IO, Any, AnyStr, Callable, Union +from typing import IO, Any, AnyStr, Callable import pytest import yatiml @@ -7,7 +7,7 @@ from ymmsl.v0_2.identity import Identifier, Reference from ymmsl.v0_2.imports import ImportKind, ImportStatement -LoadImport = Callable[[Union[str, Path, IO[AnyStr]]], Any] +LoadImport = Callable[[str | Path | IO[AnyStr]], Any] @pytest.fixture diff --git a/ymmsl/v0_2/tests/test_ports.py b/ymmsl/v0_2/tests/test_ports.py index dbeb348..15a6ff9 100644 --- a/ymmsl/v0_2/tests/test_ports.py +++ b/ymmsl/v0_2/tests/test_ports.py @@ -207,7 +207,7 @@ def test_ports_iteration() -> None: p = Ports("p", "q r", ["s", "t"], "u v") names = "pqrstuv" - for port_name, ref in zip(p, names): + for port_name, ref in zip(p, names, strict=False): assert port_name == ref diff --git a/ymmsl/v0_2/tests/test_resolver.py b/ymmsl/v0_2/tests/test_resolver.py index d39ec0c..fba689d 100644 --- a/ymmsl/v0_2/tests/test_resolver.py +++ b/ymmsl/v0_2/tests/test_resolver.py @@ -1,7 +1,7 @@ import logging import os -import sys from collections.abc import Generator +from importlib.metadata import EntryPoint, EntryPoints from pathlib import Path from unittest.mock import Mock, patch @@ -12,11 +12,6 @@ from ymmsl.v0_2.identity import Reference from ymmsl.v0_2.resolver import resolve -if sys.version_info < (3, 10): - from importlib_metadata import EntryPoint, EntryPoints -else: - from importlib.metadata import EntryPoint, EntryPoints - Ref = Reference diff --git a/ymmsl/v0_2/timeline_resolver.py b/ymmsl/v0_2/timeline_resolver.py index a97c1a4..6d4d190 100644 --- a/ymmsl/v0_2/timeline_resolver.py +++ b/ymmsl/v0_2/timeline_resolver.py @@ -136,7 +136,7 @@ def __init__( + "\n".join( f"- Port '{conduit.receiving_port()}' has timeline '{timeline}' " f"from {conduit}" - for conduit, timeline in zip(conduits, timelines) + for conduit, timeline in zip(conduits, timelines, strict=False) ) ) super().__init__(msg) @@ -326,7 +326,9 @@ def check_consistent(self) -> None: # Check consistency common_idx = len(timeline1) - num_reducers - for idx, (part1, part2) in enumerate(zip(timeline1, timeline2)): + for idx, (part1, part2) in enumerate( + zip(timeline1, timeline2, strict=False) + ): if idx < common_idx: if part1 != part2: raise ConduitTimelineError(self, conduit, timeline1, timeline2)