Repository: OpenFreeEnergy/gufe
Pull request #505: Run pyupgrade on codebase now that we support >=3.10
Author: login: mikemhenry
Created: 2025-03-08T00:04:15Z | Merged: 2025-03-17T20:33:08Z

<!--
Thank you for pull request.
Below are a few things we ask you kindly to self-check before getting a review. Remove checks that are not relevant.
-->

<!--
Please note any issues this fixes using [closing keywords]( https://help.github.com/articles/closing-issues-using-keywords/ ):
-->

<!--
see https://regro.github.io/rever-docs/news.html for details on how to add news entry (you do not need to run the rever command)
-->
Tips
* Comment "pre-commit.ci autofix" to have pre-commit.ci atomically format your PR.
  Since this will create a commit, it is best to make this comment when you are finished with your work.


Checklist
* [ ] Added a ``news`` entry

## Developers certificate of origin
- [ ] I certify that this contribution is covered by the MIT License [here](https://github.com/OpenFreeEnergy/openfe/blob/main/LICENSE) and the **Developer Certificate of Origin** at <https://developercertificate.org/>.



Diff:
diff --git a/docs/conf.py b/docs/conf.py
index 62d8bbdb9..c8da67d6a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -62,7 +62,6 @@
 autodoc_mock_imports = [
     "openff.models",
     "rdkit",
-    "networkx",
 ]
 
 
diff --git a/docs/environment.yaml b/docs/environment.yaml
index 1cdea9da5..9af1d61f3 100644
--- a/docs/environment.yaml
+++ b/docs/environment.yaml
@@ -8,5 +8,6 @@ dependencies:
 - python=3.10
 - sphinx
 - openmm
+- networkx
 - pip:
   - git+https://github.com/OpenFreeEnergy/ofe-sphinx-theme@a45f3edd5bc3e973c1a01b577c71efa1b62a65d6
diff --git a/gufe/chemicalsystem.py b/gufe/chemicalsystem.py
index 25f9d5eae..bcff65d44 100644
--- a/gufe/chemicalsystem.py
+++ b/gufe/chemicalsystem.py
@@ -12,7 +12,7 @@ class ChemicalSystem(GufeTokenizable, abc.Mapping):
     def __init__(
         self,
         components: dict[str, Component],
-        name: Optional[str] = "",
+        name: str | None = "",
     ):
         r"""A combination of Components that form a system
 
diff --git a/gufe/components/component.py b/gufe/components/component.py
index 21465ec08..664988425 100644
--- a/gufe/components/component.py
+++ b/gufe/components/component.py
@@ -26,6 +26,6 @@ def name(self) -> str:
 
     @property
     @abc.abstractmethod
-    def total_charge(self) -> Union[int, None]:
+    def total_charge(self) -> int | None:
         """Net formal charge for the ``Component``, if defined."""
         ...
diff --git a/gufe/components/explicitmoleculecomponent.py b/gufe/components/explicitmoleculecomponent.py
index e0a25e02c..e1bbc989b 100644
--- a/gufe/components/explicitmoleculecomponent.py
+++ b/gufe/components/explicitmoleculecomponent.py
@@ -63,7 +63,7 @@ def __init__(self, rdkit: RDKitMol, name: str = ""):
             )
 
         self._rdkit = rdkit
-        self._smiles: Optional[str] = None
+        self._smiles: str | None = None
         self._name = name
 
         self._check_partial_charges()
diff --git a/gufe/components/proteincomponent.py b/gufe/components/proteincomponent.py
index a23ccf096..31f48b619 100644
--- a/gufe/components/proteincomponent.py
+++ b/gufe/components/proteincomponent.py
@@ -144,7 +144,7 @@ def from_pdbx_file(cls, pdbx_file: str, name=""):
         return cls._from_openmmPDBFile(openmm_PDBFile=openmm_PDBxFile, name=name)
 
     @classmethod
-    def _from_openmmPDBFile(cls, openmm_PDBFile: Union[PDBFile, PDBxFile], name: str = ""):
+    def _from_openmmPDBFile(cls, openmm_PDBFile: PDBFile | PDBxFile, name: str = ""):
         """Converts to our internal representation (rdkit Mol)
 
         Parameters
@@ -395,7 +395,7 @@ def to_openmm_positions(self) -> omm_unit.Quantity:
 
         return openmm_pos
 
-    def to_pdb_file(self, out_path: Union[str, bytes, PathLike[str], PathLike[bytes], io.TextIOBase]) -> str:
+    def to_pdb_file(self, out_path: str | bytes | PathLike[str] | PathLike[bytes] | io.TextIOBase) -> str:
         """
         serialize protein to pdb file.
 
@@ -437,7 +437,7 @@ def to_pdb_file(self, out_path: Union[str, bytes, PathLike[str], PathLike[bytes]
 
         return out_path
 
-    def to_pdbx_file(self, out_path: Union[str, bytes, PathLike[str], PathLike[bytes], io.TextIOBase]) -> str:
+    def to_pdbx_file(self, out_path: str | bytes | PathLike[str] | PathLike[bytes] | io.TextIOBase) -> str:
         """
         serialize protein to pdbx file.
 
diff --git a/gufe/custom_json.py b/gufe/custom_json.py
index 535487f6b..0de75b132 100644
--- a/gufe/custom_json.py
+++ b/gufe/custom_json.py
@@ -5,8 +5,8 @@
 
 import functools
 import json
-from collections.abc import Iterable
-from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
+from collections.abc import Callable, Iterable
+from typing import Any, Dict, List, Optional, Tuple, Type, Union
 
 
 class JSONCodec:
@@ -36,10 +36,10 @@ class JSONCodec:
 
     def __init__(
         self,
-        cls: Union[type, None],
+        cls: type | None,
         to_dict: Callable[[Any], dict],
         from_dict: Callable[[dict], Any],
-        is_my_obj: Optional[Callable[[Any], bool]] = None,
+        is_my_obj: Callable[[Any], bool] | None = None,
         is_my_dict=None,
     ):
         if is_my_obj is None:
diff --git a/gufe/custom_typing.py b/gufe/custom_typing.py
index 38f916256..0452db57f 100644
--- a/gufe/custom_typing.py
+++ b/gufe/custom_typing.py
@@ -1,13 +1,8 @@
 # This code is part of OpenFE and is licensed under the MIT license.
 # For details, see https://github.com/OpenFreeEnergy/gufe
 
-from typing import TypeVar
+from typing import TypeAlias
 
 from rdkit import Chem
 
-try:
-    from typing import TypeAlias  # type: ignore
-except ImportError:
-    from typing_extensions import TypeAlias
-
 RDKitMol: TypeAlias = Chem.rdchem.Mol
diff --git a/gufe/ligandnetwork.py b/gufe/ligandnetwork.py
index 5345a71d0..dfefb76ed 100644
--- a/gufe/ligandnetwork.py
+++ b/gufe/ligandnetwork.py
@@ -6,7 +6,7 @@
 import json
 from collections.abc import Iterable
 from itertools import chain
-from typing import FrozenSet, Iterable, Optional, Union
+from typing import FrozenSet, Optional, Union
 
 import networkx as nx
 
@@ -183,8 +183,8 @@ def from_graphml(cls, graphml_str: str) -> LigandNetwork:
     def enlarge_graph(
         self,
         *,
-        edges: Optional[Iterable[LigandAtomMapping]] = None,
-        nodes: Optional[Iterable[SmallMoleculeComponent]] = None,
+        edges: Iterable[LigandAtomMapping] | None = None,
+        nodes: Iterable[SmallMoleculeComponent] | None = None,
     ) -> LigandNetwork:
         """Create a new network with the given edges and nodes added.
 
@@ -211,8 +211,8 @@ def enlarge_graph(
     def trim_graph(
         self,
         *,
-        edges: Optional[Iterable[LigandAtomMapping]] = None,
-        nodes: Optional[Iterable[SmallMoleculeComponent]] = None,
+        edges: Iterable[LigandAtomMapping] | None = None,
+        nodes: Iterable[SmallMoleculeComponent] | None = None,
     ) -> LigandNetwork:
         """Create a new network with the given edges and nodes removed.
 
diff --git a/gufe/network.py b/gufe/network.py
index 18bb06d94..c5b1c68cb 100644
--- a/gufe/network.py
+++ b/gufe/network.py
@@ -15,8 +15,8 @@
 class AlchemicalNetwork(GufeTokenizable):
     _edges: frozenset[Transformation]
     _nodes: frozenset[ChemicalSystem]
-    _name: Optional[str]
-    _graph: Optional[nx.MultiDiGraph]  # lazily created and cached
+    _name: str | None
+    _graph: nx.MultiDiGraph | None  # lazily created and cached
 
     """A network with all the information needed for a simulation campaign.
 
@@ -34,9 +34,9 @@ class AlchemicalNetwork(GufeTokenizable):
 
     def __init__(
         self,
-        edges: Optional[Iterable[Transformation]] = None,
-        nodes: Optional[Iterable[ChemicalSystem]] = None,
-        name: Optional[str] = None,
+        edges: Iterable[Transformation] | None = None,
+        nodes: Iterable[ChemicalSystem] | None = None,
+        name: str | None = None,
     ):
         self._edges = frozenset(edges) if edges else frozenset()
         self._name = name
@@ -89,7 +89,7 @@ def nodes(self) -> frozenset[ChemicalSystem]:
         return self._nodes
 
     @property
-    def name(self) -> Optional[str]:
+    def name(self) -> str | None:
         """Optional identifier for the network."""
         return self._name
 
diff --git a/gufe/protocols/protocol.py b/gufe/protocols/protocol.py
index 006717da2..0e15b624c 100644
--- a/gufe/protocols/protocol.py
+++ b/gufe/protocols/protocol.py
@@ -172,8 +172,8 @@ def _create(
         self,
         stateA: ChemicalSystem,
         stateB: ChemicalSystem,
-        mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]],
-        extends: Optional[ProtocolDAGResult] = None,
+        mapping: ComponentMapping | list[ComponentMapping] | None,
+        extends: ProtocolDAGResult | None = None,
     ) -> list[ProtocolUnit]:
         """Method to override in custom :class:`Protocol` subclasses.
 
@@ -208,10 +208,10 @@ def create(
         *,
         stateA: ChemicalSystem,
         stateB: ChemicalSystem,
-        mapping: Optional[Union[ComponentMapping, list[ComponentMapping], dict[str, ComponentMapping]]],
-        extends: Optional[ProtocolDAGResult] = None,
-        name: Optional[str] = None,
-        transformation_key: Optional[GufeKey] = None,
+        mapping: ComponentMapping | list[ComponentMapping] | dict[str, ComponentMapping] | None,
+        extends: ProtocolDAGResult | None = None,
+        name: str | None = None,
+        transformation_key: GufeKey | None = None,
     ) -> ProtocolDAG:
         r"""Prepare a `ProtocolDAG` with all information required for execution.
 
diff --git a/gufe/protocols/protocoldag.py b/gufe/protocols/protocoldag.py
index 9a23e8806..6dc136db6 100644
--- a/gufe/protocols/protocoldag.py
+++ b/gufe/protocols/protocoldag.py
@@ -21,16 +21,16 @@
 class DAGMixin:
     _protocol_units: list[ProtocolUnit]
 
-    _name: Optional[str]
+    _name: str | None
     _graph: nx.DiGraph
 
     # labels for identifying source of this DAG
 
     ## key of the Transformation that this DAG corresponds to
-    _transformation_key: Union[GufeKey, None]
+    _transformation_key: GufeKey | None
 
     ## key of the ProtocolDAG this DAG extends
-    _extends_key: Optional[GufeKey]
+    _extends_key: GufeKey | None
 
     @staticmethod
     def _build_graph(nodes):
@@ -49,7 +49,7 @@ def _iterate_dag_order(graph):
         return reversed(list(nx.lexicographical_topological_sort(graph, key=lambda pu: pu.key)))
 
     @property
-    def name(self) -> Optional[str]:
+    def name(self) -> str | None:
         """Optional identifier."""
         return self._name
 
@@ -69,7 +69,7 @@ def protocol_units(self) -> list[ProtocolUnit]:
         return list(self._iterate_dag_order(self._graph))
 
     @property
-    def transformation_key(self) -> Union[GufeKey, None]:
+    def transformation_key(self) -> GufeKey | None:
         """
         The `GufeKey` of the `Transformation` this object performs.
 
@@ -82,7 +82,7 @@ def transformation_key(self) -> Union[GufeKey, None]:
         return self._transformation_key
 
     @property
-    def extends_key(self) -> Union[GufeKey, None]:
+    def extends_key(self) -> GufeKey | None:
         """The `GufeKey` of the `ProtocolDAGResult` this object extends.
 
         If `None`, then this object does not extend from a result at all.
@@ -112,9 +112,9 @@ def __init__(
         *,
         protocol_units: list[ProtocolUnit],
         protocol_unit_results: list[ProtocolUnitResult],
-        transformation_key: Union[GufeKey, None],
-        extends_key: Optional[GufeKey] = None,
-        name: Optional[str] = None,
+        transformation_key: GufeKey | None,
+        extends_key: GufeKey | None = None,
+        name: str | None = None,
     ):
         self._name = name
         self._protocol_units = protocol_units
@@ -296,9 +296,9 @@ def __init__(
         self,
         *,
         protocol_units: list[ProtocolUnit],
-        transformation_key: Union[GufeKey, None],
-        extends_key: Optional[GufeKey] = None,
-        name: Optional[str] = None,
+        transformation_key: GufeKey | None,
+        extends_key: GufeKey | None = None,
+        name: str | None = None,
     ):
         """Create a new `ProtocolDAG`
 
@@ -443,7 +443,7 @@ def execute_DAG(
 
 
 def _pu_to_pur(
-    inputs: Union[dict[str, Any], list[Any], ProtocolUnit],
+    inputs: dict[str, Any] | list[Any] | ProtocolUnit,
     mapping: dict[GufeKey, ProtocolUnitResult],
 ):
     """Convert each `ProtocolUnit` found within `inputs` to its corresponding
diff --git a/gufe/settings/models.py b/gufe/settings/models.py
index 734fcb56f..2d35f5111 100644
--- a/gufe/settings/models.py
+++ b/gufe/settings/models.py
@@ -112,8 +112,8 @@ class ThermoSettings(SettingsBaseModel):
     pressure: FloatQuantity["standard_atmosphere"] = Field(
         None, description="Simulation pressure, default units standard atmosphere (atm)"
     )
-    ph: Union[PositiveFloat, None] = Field(None, description="Simulation pH")
-    redox_potential: Optional[float] = Field(None, description="Simulation redox potential")
+    ph: PositiveFloat | None = Field(None, description="Simulation pH")
+    redox_potential: float | None = Field(None, description="Simulation redox potential")
 
 
 class BaseForceFieldSettings(SettingsBaseModel, abc.ABC):
@@ -146,7 +146,7 @@ class Config:
 
         pass
 
-    constraints: Optional[str] = "hbonds"
+    constraints: str | None = "hbonds"
     """Constraints to be applied to system.
        One of 'hbonds', 'allbonds', 'hangles' or None, default 'hbonds'"""
     rigid_water: bool = True
diff --git a/gufe/storage/externalresource/filestorage.py b/gufe/storage/externalresource/filestorage.py
index e2b5a54bd..47d296ff6 100644
--- a/gufe/storage/externalresource/filestorage.py
+++ b/gufe/storage/externalresource/filestorage.py
@@ -11,7 +11,7 @@
 
 # TODO: this should use pydantic to check init inputs
 class FileStorage(ExternalStorage):
-    def __init__(self, root_dir: Union[pathlib.Path, str]):
+    def __init__(self, root_dir: pathlib.Path | str):
         self.root_dir = pathlib.Path(root_dir)
 
     def _exists(self, location):
@@ -54,7 +54,7 @@ def _delete(self, location):
     def _as_path(self, location):
         return self.root_dir / pathlib.Path(location)
 
-    def _get_location(self, path: Union[str, pathlib.Path]) -> str:
+    def _get_location(self, path: str | pathlib.Path) -> str:
         # this is essentially the reverse behavior of _as_path
         path = pathlib.Path(path)
         relpath = path.relative_to(self.root_dir.resolve())
diff --git a/gufe/tests/test_protocol.py b/gufe/tests/test_protocol.py
index 2fed18dca..8f91a19ac 100644
--- a/gufe/tests/test_protocol.py
+++ b/gufe/tests/test_protocol.py
@@ -111,8 +111,8 @@ def _create(
         self,
         stateA: ChemicalSystem,
         stateB: ChemicalSystem,
-        mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]] = None,
-        extends: Optional[ProtocolDAGResult] = None,
+        mapping: ComponentMapping | list[ComponentMapping] | None = None,
+        extends: ProtocolDAGResult | None = None,
     ) -> list[ProtocolUnit]:
 
         # rip apart `extends` if needed to feed into `InitializeUnit`
@@ -170,8 +170,8 @@ def _create(
         self,
         stateA: ChemicalSystem,
         stateB: ChemicalSystem,
-        mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]] = None,
-        extends: Optional[ProtocolDAGResult] = None,
+        mapping: ComponentMapping | list[ComponentMapping] | None = None,
+        extends: ProtocolDAGResult | None = None,
     ) -> list[ProtocolUnit]:
 
         # convert protocol inputs into starting points for independent simulations
@@ -433,7 +433,7 @@ def instance(self, protocol_dag):
         def test_protocol_unit_results(self, instance: ProtocolDAGResult):
             # ensure that protocolunitresults are given in-order based on DAG
             # dependencies
-            checked: list[Union[ProtocolUnitResult, ProtocolUnitFailure]] = []
+            checked: list[ProtocolUnitResult | ProtocolUnitFailure] = []
             for pur in instance.protocol_unit_results:
                 assert set(pur.dependencies).issubset(checked)
                 checked.append(pur)
@@ -557,8 +557,8 @@ def _create(
         self,
         stateA: ChemicalSystem,
         stateB: ChemicalSystem,
-        mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]] = None,
-        extends: Optional[ProtocolDAGResult] = None,
+        mapping: ComponentMapping | list[ComponentMapping] | None = None,
+        extends: ProtocolDAGResult | None = None,
     ) -> list[ProtocolUnit]:
         return [NoDepUnit(settings=self.settings, val=i) for i in range(3)]
 
@@ -696,7 +696,7 @@ def test_missing_result(self, units, successes, failures):
 
         assert not dagresult.ok()
 
-        with pytest.raises(ProtocolUnitFailureError, match="No success for `protocol_unit`:NoDepUnit\(None\) found"):
+        with pytest.raises(ProtocolUnitFailureError, match=r"No success for `protocol_unit`:NoDepUnit\(None\) found"):
             dagresult.unit_to_result(units[2])
 
     def test_plenty_of_fails(self, units, successes, failures):
@@ -725,12 +725,12 @@ def test_foreign_objects(self, units, successes):
             transformation_key=None,
         )
 
-        with pytest.raises(MissingUnitResultError, match="No such `protocol_unit`:NoDepUnit\(None\) present"):
+        with pytest.raises(MissingUnitResultError, match=r"No such `protocol_unit`:NoDepUnit\(None\) present"):
             dagresult.unit_to_result(units[2])
-        with pytest.raises(MissingUnitResultError, match="No such `protocol_unit`:NoDepUnit\(None\) present"):
+        with pytest.raises(MissingUnitResultError, match=r"No such `protocol_unit`:NoDepUnit\(None\) present"):
             dagresult.unit_to_all_results(units[2])
         with pytest.raises(
-            MissingUnitResultError, match="No such `protocol_unit_result`:ProtocolUnitResult\(None\) present"
+            MissingUnitResultError, match=r"No such `protocol_unit_result`:ProtocolUnitResult\(None\) present"
         ):
             dagresult.result_to_unit(successes[2])
 
diff --git a/gufe/tests/test_serialization_migration.py b/gufe/tests/test_serialization_migration.py
index 69d71d4f2..11991a8e6 100644
--- a/gufe/tests/test_serialization_migration.py
+++ b/gufe/tests/test_serialization_migration.py
@@ -152,9 +152,9 @@ def _to_dict(self):
 
 
 class MigrationTester(GufeTokenizableTestsMixin):
-    input_dict: Optional[dict[str, Any]] = None
+    input_dict: dict[str, Any] | None = None
     """Initial input dict (except class name info)"""
-    kwargs: Optional[dict[str, Any]] = None
+    kwargs: dict[str, Any] | None = None
     """kwargs to create an equivalent object from scratch"""
 
     repr = None
diff --git a/gufe/tests/test_tokenization.py b/gufe/tests/test_tokenization.py
index 55d2512b7..a24fec944 100644
--- a/gufe/tests/test_tokenization.py
+++ b/gufe/tests/test_tokenization.py
@@ -92,7 +92,7 @@ class GufeTokenizableTestsMixin(abc.ABC):
 
     # set this to the `GufeTokenizable` subclass you are testing
     cls: type[GufeTokenizable]
-    repr: Optional[str]
+    repr: str | None
 
     @pytest.fixture
     def instance(self):
diff --git a/gufe/tokenization.py b/gufe/tokenization.py
index 1fa62876c..97b4a3afa 100644
--- a/gufe/tokenization.py
+++ b/gufe/tokenization.py
@@ -649,7 +649,7 @@ def from_keyed_chain(cls, keyed_chain: list[tuple[str, dict]]):
         """
         return KeyedChain(keyed_chain=keyed_chain).to_gufe()
 
-    def to_json(self, file: Optional[PathLike | TextIO] = None) -> None | str:
+    def to_json(self, file: PathLike | TextIO | None = None) -> None | str:
         """
         Generate a JSON keyed chain representation.
 
@@ -681,7 +681,7 @@ def to_json(self, file: Optional[PathLike | TextIO] = None) -> None | str:
         return None
 
     @classmethod
-    def from_json(cls, file: Optional[PathLike | TextIO] = None, content: Optional[str] = None):
+    def from_json(cls, file: PathLike | TextIO | None = None, content: str | None = None):
         """
         Generate an instance from JSON keyed chain representation.
 
@@ -744,7 +744,7 @@ def token(self) -> str:
         return self.split("-")[1]
 
 
-def gufe_objects_from_shallow_dict(obj: Union[list, dict, GufeTokenizable]) -> list[GufeTokenizable]:
+def gufe_objects_from_shallow_dict(obj: list | dict | GufeTokenizable) -> list[GufeTokenizable]:
     """Find GufeTokenizables within a shallow dict.
 
     This function recursively looks through the list/dict structures encoding
@@ -984,7 +984,7 @@ def get_class(module: str, qualname: str):
         return cls
 
 
-def modify_dependencies(obj: Union[dict, list], modifier, is_mine, mode, top=True):
+def modify_dependencies(obj: dict | list, modifier, is_mine, mode, top=True):
     """
     Parameters
     ----------
diff --git a/gufe/transformations/transformation.py b/gufe/transformations/transformation.py
index 02f2cdf48..84b2de2a1 100644
--- a/gufe/transformations/transformation.py
+++ b/gufe/transformations/transformation.py
@@ -16,12 +16,12 @@
 
 class TransformationBase(GufeTokenizable):
     _protocol: Protocol
-    _name: Optional[str]
+    _name: str | None
 
     def __init__(
         self,
         protocol: Protocol,
-        name: Optional[str] = None,
+        name: str | None = None,
     ):
         """Transformation base class.
 
@@ -41,7 +41,7 @@ def _defaults(cls):
         return super()._defaults()
 
     @property
-    def name(self) -> Optional[str]:
+    def name(self) -> str | None:
         """Optional identifier for the transformation; used as part of its hash.
 
         Set this to a unique value if adding multiple, otherwise identical
@@ -70,8 +70,8 @@ def stateB(self) -> ChemicalSystem:
     def create(
         self,
         *,
-        extends: Optional[ProtocolDAGResult] = None,
-        name: Optional[str] = None,
+        extends: ProtocolDAGResult | None = None,
+        name: str | None = None,
     ) -> ProtocolDAG:
         """
         Returns a :class:`.ProtocolDAG` executing this ``Transformation.protocol``.
@@ -91,7 +91,7 @@ def protocol(self) -> Protocol:
         return self._protocol
 
     def gather(self, protocol_dag_results: Iterable[ProtocolDAGResult]) -> ProtocolResult:
-        """Gather multiple :class:`.ProtocolDAGResult` \s into a single
+        r"""Gather multiple :class:`.ProtocolDAGResult` \s into a single
         :class:`.ProtocolResult`.
 
         Parameters
@@ -151,18 +151,18 @@ class Transformation(TransformationBase):
     _stateA: ChemicalSystem
     _stateB: ChemicalSystem
     _protocol: Protocol
-    _mapping: Optional[Union[ComponentMapping, list[ComponentMapping]]]
-    _name: Optional[str]
+    _mapping: ComponentMapping | list[ComponentMapping] | None
+    _name: str | None
 
     def __init__(
         self,
         stateA: ChemicalSystem,
         stateB: ChemicalSystem,
         protocol: Protocol,
-        mapping: Optional[Union[ComponentMapping, list[ComponentMapping], dict[str, ComponentMapping]]] = None,
-        name: Optional[str] = None,
+        mapping: ComponentMapping | list[ComponentMapping] | dict[str, ComponentMapping] | None = None,
+        name: str | None = None,
     ):
-        """Two chemical states with a method for estimating the free energy
+        r"""Two chemical states with a method for estimating the free energy
         difference between them.
 
         Connects two :class:`.ChemicalSystem` objects, with directionality, and
@@ -211,7 +211,7 @@ def stateB(self) -> ChemicalSystem:
         return self._stateB
 
     @property
-    def mapping(self) -> Optional[Union[ComponentMapping, list[ComponentMapping]]]:
+    def mapping(self) -> ComponentMapping | list[ComponentMapping] | None:
         """The mappings relevant for this Transformation"""
         return self._mapping
 
@@ -227,8 +227,8 @@ def _to_dict(self) -> dict:
     def create(
         self,
         *,
-        extends: Optional[ProtocolDAGResult] = None,
-        name: Optional[str] = None,
+        extends: ProtocolDAGResult | None = None,
+        name: str | None = None,
     ) -> ProtocolDAG:
         """
         Returns a ``ProtocolDAG`` executing this ``Transformation.protocol``.
@@ -246,13 +246,13 @@ def create(
 class NonTransformation(TransformationBase):
     _system: ChemicalSystem
     _protocol: Protocol
-    _name: Optional[str]
+    _name: str | None
 
     def __init__(
         self,
         system: ChemicalSystem,
         protocol: Protocol,
-        name: Optional[str] = None,
+        name: str | None = None,
     ):
         """A non-alchemical edge of an alchemical network.
 
@@ -317,8 +317,8 @@ def _to_dict(self) -> dict:
     def create(
         self,
         *,
-        extends: Optional[ProtocolDAGResult] = None,
-        name: Optional[str] = None,
+        extends: ProtocolDAGResult | None = None,
+        name: str | None = None,
     ) -> ProtocolDAG:
         """
         Returns a ``ProtocolDAG`` executing this ``NonTransformation.protocol``.
diff --git a/gufe/visualization/mapping_visualization.py b/gufe/visualization/mapping_visualization.py
index ac110b8a3..279d2a913 100644
--- a/gufe/visualization/mapping_visualization.py
+++ b/gufe/visualization/mapping_visualization.py
@@ -97,7 +97,7 @@ def _draw_molecules(
     atom_colors: Collection[dict[Any, tuple[float, float, float, float]]],
     bond_colors: Collection[dict[int, tuple[float, float, float, float]]],
     highlight_color: tuple[float, float, float, float],
-    atom_mapping: Optional[dict[tuple[int, int], dict[int, int]]] = None,
+    atom_mapping: dict[tuple[int, int], dict[int, int]] | None = None,
 ) -> str:
     """
     Internal method to visualize a molecule, possibly with mapping info


Reviews:
user: login: IAlibay
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T00:15:23Z
user: login: mikemhenry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T20:14:53Z
user: login: IAlibay
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-08T20:17:43Z
user: login: mikemhenry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T20:18:57Z
user: login: mikemhenry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-08T20:19:19Z
user: login: mikemhenry
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T00:43:07Z
user: login: atravitz
body: looks good!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T17:47:36Z
user: login: ianmkenney
body: Looks great! Very excited to lose the invalid escape sequences.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T15:54:39Z
user: login: dotsdl
body: Looks great @mikemhenry! Merge if satisfied!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T20:32:30Z

Review comments:
diff_hunk: @@ -12,7 +12,7 @@ class ChemicalSystem(GufeTokenizable, abc.Mapping):
     def __init__(
         self,
         components: dict[str, Component],
-        name: Optional[str] = "",
+        name: str | None = "",
path: gufe/chemicalsystem.py
user: login: IAlibay
body: So I get that this is the new syntax, but `Optional` is, in my opinion, way more descriptive than `a | None`. 

`Optional[a]` > `a | None` > `Union[a, None]`
created_at: 2025-03-08T00:15:23Z
author_association: MEMBER
diff_hunk: @@ -12,7 +12,7 @@ class ChemicalSystem(GufeTokenizable, abc.Mapping):
     def __init__(
         self,
         components: dict[str, Component],
-        name: Optional[str] = "",
+        name: str | None = "",
path: gufe/chemicalsystem.py
user: login: mikemhenry
body: It does save having to import `Optional` -- I think Optional is more descriptive, but ` | None` is more explicit. Since `Optional` means "can be `None`" in this context, we are now exhaustively listing all the types it can be. I think in a doc string "Optional" is way more descriptive (I would never say "can be None" in a doc string, I would say the argument is optional) but in the code, I like the explicitness. 
created_at: 2025-03-08T20:14:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,7 +12,7 @@ class ChemicalSystem(GufeTokenizable, abc.Mapping):
     def __init__(
         self,
         components: dict[str, Component],
-        name: Optional[str] = "",
+        name: str | None = "",
path: gufe/chemicalsystem.py
user: login: IAlibay
body: :( I had finally given up on my love for `Union`
created_at: 2025-03-08T20:17:43Z
author_association: MEMBER
diff_hunk: @@ -8,6 +8,6 @@
 try:
     from typing import TypeAlias  # type: ignore
 except ImportError:
-    from typing_extensions import TypeAlias
+    from typing import TypeAlias
path: gufe/custom_typing.py
user: login: mikemhenry
body: lol, we can now remove this little code block since 
```
try:
    from typing import TypeAlias  # type: ignore
except ImportError:
    from typing import TypeAlias
```
is just madness 
created_at: 2025-03-08T20:18:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -8,6 +8,6 @@
 try:
     from typing import TypeAlias  # type: ignore
 except ImportError:
-    from typing_extensions import TypeAlias
+    from typing import TypeAlias
path: gufe/custom_typing.py
user: login: mikemhenry
body: "well sure it didn't work, but try it again!"
created_at: 2025-03-08T20:19:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -725,12 +725,12 @@ def test_foreign_objects(self, units, successes):
             transformation_key=None,
         )
 
-        with pytest.raises(MissingUnitResultError, match="No such `protocol_unit`:NoDepUnit\(None\) present"):
+        with pytest.raises(MissingUnitResultError, match=r"No such `protocol_unit`:NoDepUnit\(None\) present"):
path: gufe/tests/test_protocol.py
user: login: mikemhenry
body: Ah https://github.com/asottile/pyupgrade?tab=readme-ov-file#invalid-escape-sequences

This is nice so python doesn't think we are trying to escape things in the string 
created_at: 2025-03-09T00:43:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,7 +12,7 @@ class ChemicalSystem(GufeTokenizable, abc.Mapping):
     def __init__(
         self,
         components: dict[str, Component],
-        name: Optional[str] = "",
+        name: str | None = "",
path: gufe/chemicalsystem.py
user: login: dotsdl
body: I actually think this syntax is an improvement over use of `Optional`. When `typing` first started getting use I found `Optional` so confusing, since it can technically be used for *required* arguments (all it means is "can be `None`"!), which just felt oxymoronic.
created_at: 2025-03-17T19:49:52Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-03-08T00:07:03Z
body: ## [Codecov](https://app.codecov.io/gh/OpenFreeEnergy/gufe/pull/505?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenFreeEnergy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.71%. Comparing base [(`0df4963`)](https://app.codecov.io/gh/OpenFreeEnergy/gufe/commit/0df49630d5fa86c9dcf65b2124c450e2cee8015c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenFreeEnergy) to head [(`1a92b74`)](https://app.codecov.io/gh/OpenFreeEnergy/gufe/commit/1a92b74c74c3953dd46bf459fe655c3a1c579d92?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenFreeEnergy).
> Report is 21 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #505      +/-   ##
==========================================
+ Coverage   98.62%   98.71%   +0.08%     
==========================================
  Files          39       39              
  Lines        2179     2175       -4     
==========================================
- Hits         2149     2147       -2     
+ Misses         30       28       -2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/OpenFreeEnergy/gufe/pull/505?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenFreeEnergy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OpenFreeEnergy).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: mikemhenry
created_at: 2025-03-08T20:10:06Z
body: pre-commit.ci autofix
author_association: CONTRIBUTOR
user: login: mikemhenry
created_at: 2025-03-08T20:10:55Z
body: Interesting this broke the doc build?
author_association: CONTRIBUTOR
user: login: mikemhenry
created_at: 2025-03-12T23:46:56Z
body: weird RTD is giving me a problem `sphinx.ext.autosummary.ImportExceptionGroup: no module named gufe.network`
author_association: CONTRIBUTOR
user: login: mikemhenry
created_at: 2025-03-12T23:47:56Z
body: Will want to work on this a bit more to fix the docs + also cleanup some cruft, like the try/except stuff
author_association: CONTRIBUTOR
user: login: mikemhenry
created_at: 2025-03-13T23:40:49Z
body: This is now ready to go!
author_association: CONTRIBUTOR
user: login: mikemhenry
created_at: 2025-03-13T23:41:24Z
body: I wanted to get @dotsdl and @ianmkenney sign off on it as well since it is kinda a sweeping change
author_association: CONTRIBUTOR
user: login: mikemhenry
created_at: 2025-03-14T15:58:08Z
body: Yah for my next trick I'm going to look into all the warnings we are spitting out in CI 
author_association: CONTRIBUTOR