Repository: openforcefield/openff-toolkit

Issue:
Allow setting `Molecule.name` in constructor and some methods
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

`Molecule.name` is not relevant in all use cases but critical in some, i.e. exporting to GROMACS and LAMMPS files. When constructing a `Topology` or collection of `Molecule`s that need names, it's a bit clunky.

```python3
from openff.toolkit import Molecule

ligand = Molecule.from_smiles(SMILES)
ligand.name = "LIG"
```

or when dealing with multiple molecules
```python3
topology = Topology.from_molecules(
    molecules=[Molecule.from_smiles(smiles) for smiles in ["C", "CC", "CO"]]
)

for index, name in enumerate(["MET", "ETH", "ETO"]):
    topology.molecule(index).name = name
```

I also found that `Molecule.__init__` allows anything to be passed into its constructor, but only keyword arguments go nowhere and only certain named arguments can be processed:

```python
class FrozenMolecule(Serializable):
    def __init__(
        self,
        other=None,
        file_format: Optional[str] = None,
        toolkit_registry: TKR = GLOBAL_TOOLKIT_REGISTRY,
        allow_undefined_stereo: bool = False,
    ):
        ...

class Molecule(FrozenMolecule):
    def __init__(self, *args, **kwargs):
        super(Molecule, self).__init__(*args, **kwargs)
```

As a result, this trick doesn't work:

```python
>>> Molecule(name='test').name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/mattthompson/mambaforge/envs/openff-interchange-env/lib/python3.10/site-packages/openff/toolkit/topology/molecule.py", line 5150, in __init__
    super(Molecule, self).__init__(*args, **kwargs)
TypeError: FrozenMolecule.__init__() got an unexpected keyword argument 'name'
```

**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->

It's be nice to do this in one pass, i.e.

```python3
ligand = Molecule.from_smiles(SMILES, name="LIG")

topology = Topology.from_molecules(
    molecules=[
        Molecule.from_smiles(smiles, name=name)
        for smiles, name in zip(["C", "CC", "CO"], ["MET", "ETH", "ETO"])
    ]
)
```

Would a patch that adds `name` as a named argument to `Molecule.__init__` be accepted? It would default to the current value of an empty string. This could also be wired to some `Molecule.from_X` methods without breaking existing behavior, I think.


**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
Related #1028 for a more specific use case

Discussion:
codecov[bot]: ## [Codecov](https://codecov.io/gh/openforcefield/openff-toolkit/pull/1567?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openforcefield) Report
> Merging [#1567](https://codecov.io/gh/openforcefield/openff-toolkit/pull/1567?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openforcefield) (3c70a53) into [main](https://codecov.io/gh/openforcefield/openff-toolkit/commit/485ae2ab8e9598db102585dfd2bc71f433469454?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openforcefield) (485ae2a) will **decrease** coverage by `12.02%`.
> The diff coverage is `75.00%`.

> :exclamation: Current head 3c70a53 differs from pull request most recent head 87531e4. Consider uploading reports for the commit 87531e4 to get more accurate results

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


</details>

Patch:
diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml
index 26a8c402..ee228b5c 100644
--- a/.github/workflows/examples.yml
+++ b/.github/workflows/examples.yml
@@ -139,6 +139,7 @@ jobs:
             NB_ARGS+=" --ignore=examples/check_dataset_parameter_coverage"
             NB_ARGS+=" --ignore=examples/conformer_energies"
             NB_ARGS+=" --ignore=examples/using_smirnoff_in_amber_or_gromacs"
+            NB_ARGS+=" --ignore=examples/toolkit_showcase"
           fi
 
           python -m pytest $PYTEST_ARGS $NB_ARGS examples
diff --git a/docs/releasehistory.md b/docs/releasehistory.md
index 16aac768..6e995db9 100644
--- a/docs/releasehistory.md
+++ b/docs/releasehistory.md
@@ -9,6 +9,7 @@ Releases follow the `major.minor.micro` scheme recommended by [PEP440](https://w
 ## Current development
 
 ### New features
+- [PR #1567](https://github.com/openforcefield/openff-toolkit/pull/1567): Allows setting `Molecule.name` in `Molecule.from_smiles`, `from_inchi`, `from_polymer_pdb`, and `from_pdb_and_smiles`.
 - [PR #1565](https://github.com/openforcefield/openff-toolkit/pull/1565): Adds `Topology.from_pdb`
 
 ### Behavior changes
@@ -19,6 +20,9 @@ Releases follow the `major.minor.micro` scheme recommended by [PEP440](https://w
 - [PR #1564] Improve documentation of conformer selection in `Molecule.assign_partial_charges()`
 - [PR #1574](https://github.com/openforcefield/openff-toolkit/pull/1574) Fix class method signature rendering throughout API docs
 
+### Examples updates
+- [PR #1575](https://github.com/openforcefield/openff-toolkit/pull/1575): The Toolkit Showcase example has been simplified via use of `Topology.from_pdb`
+
 ## 0.12.1
 
 ### New features
diff --git a/examples/toolkit_showcase/toolkit_showcase.ipynb b/examples/toolkit_showcase/toolkit_showcase.ipynb
index 65d4b302..dfcd15b8 100644
--- a/examples/toolkit_showcase/toolkit_showcase.ipynb
+++ b/examples/toolkit_showcase/toolkit_showcase.ipynb
@@ -49,7 +49,6 @@
     "import numpy as np\n",
     "import openmm\n",
     "from openff.units import Quantity, unit\n",
-    "from openff.units.openmm import from_openmm\n",
     "from openmm import unit as openmm_unit\n",
     "from pdbfixer import PDBFixer\n",
     "\n",
@@ -201,66 +200,14 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "An SDF file with an entire protein would be huge, and finding or constructing one in the first place would be tricky, so for polymers the Toolkit supports inferring chemical information from PDB files. Connectivity can be taken from residue and atom names or CONECT records, and formal charges and bond orders are inferred from a substructure dictionary distributed with the Toolkit. To help prevent ambiguities and mistakes, the toolkit requires the protein to be the only molecule in the PDB file.\n",
-    "\n",
-    "Our prepared PDB file has crystallographic waters, so we need to strip them out before we can load the protein. Don't worry! We'll pick them back up after we've created the receptor molecule. There's a million ways to break up a PDB; here, we'll use MDTraj."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# Load the receptor with crystallographic waters\n",
-    "protein_xtalwater_mdtraj = mdtraj.load_pdb(receptor_path)\n",
-    "\n",
-    "# Get the indices for the protein's atoms\n",
-    "# See https://www.mdtraj.org/1.9.7/atom_selection.html\n",
-    "protein_atom_indices = protein_xtalwater_mdtraj.top.select(\"resname != HOH\")\n",
-    "\n",
-    "# Slice out just the protein and save it to a new PDB\n",
-    "protein_mdtraj = protein_xtalwater_mdtraj.atom_slice(protein_atom_indices)\n",
-    "protein_mdtraj.save_pdb(\"receptor.pdb\")"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Now that we have the receptor alone in a PDB, we can load it into the toolkit. Once that's done, it's just like any other `Molecule`; you can even create a SMILES for it!"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "scrolled": true
-   },
-   "outputs": [],
-   "source": [
-    "receptor = Molecule.from_polymer_pdb(\"receptor.pdb\")\n",
-    "\n",
-    "# # This is not a good idea\n",
-    "# receptor.to_smiles()"
+    "An SDF file with an entire protein would be huge, and finding or constructing one in the first place would be tricky, so for polymers the Toolkit supports inferring chemical information from PDB files. Connectivity can be taken from residue and atom names for supported molecules, and we can supply a list of arbitrary molecules that are in the PDB which are then identified via CONECT records."
    ]
   },
   {
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "<div class=\"alert alert-info\" style=\"max-width: 700px; margin-left: auto; margin-right: auto;\">\n",
-    "    ℹ️ The warning here is just because MDTraj lost track of the insertion code for the NME cap; we don't need to worry about it.\n",
-    "</div>\n"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Now we'll solvate our system. [PDBFixer] makes it easy to add water and salt to our original receptor PDB, which already includes crystallographic waters. PDBFixer can also manage a number of other common processes, like restoring missing hydrogens or other atoms and even adding a membrane.\n",
-    "\n",
-    "Being based on OpenMM, PDBFixer is also very convenient for loading into the Toolkit without writing to disk. Since an OpenMM topology stores bond parameters rather than connectivity, we need to provide a list of the possible molecules that can be found in the file so that the Toolkit can match them to the atom connectivity graph implied by the topology. This is why we needed to load the receptor in on its own previously.\n",
+    "First we'll solvate our system. [PDBFixer] makes it easy to add water and salt to our original receptor PDB, which already includes crystallographic waters. PDBFixer can also manage a number of other common processes, like restoring missing hydrogens or other atoms and even adding a membrane. We can then save it to disk and load the resulting PDB into a topology!\n",
     "\n",
     "[PDBFixer]: https://htmlpreview.github.io/?https://github.com/openmm/pdbfixer/blob/master/Manual.html"
    ]
@@ -276,16 +223,10 @@
     "    padding=1.0 * openmm_unit.nanometer, ionicStrength=0.15 * openmm_unit.molar\n",
     ")\n",
     "\n",
-    "top = Topology.from_openmm(\n",
-    "    fixer.topology,\n",
-    "    unique_molecules=[\n",
-    "        receptor,\n",
-    "        Molecule.from_smiles(\"O\"),\n",
-    "        Molecule.from_smiles(\"[Na+]\"),\n",
-    "        Molecule.from_smiles(\"[Cl-]\"),\n",
-    "    ],\n",
-    ")\n",
-    "top.set_positions(from_openmm(fixer.positions))"
+    "with open(\"receptor_solvated.pdb\", \"w\") as f:\n",
+    "    openmm.app.PDBFile.writeFile(fixer.topology, fixer.positions, f)\n",
+    "\n",
+    "top = Topology.from_pdb(\"receptor_solvated.pdb\")"
    ]
   },
   {
@@ -757,9 +698,9 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.10.6"
+   "version": "3.10.10"
   }
  },
  "nbformat": 4,
  "nbformat_minor": 4
-}
\ No newline at end of file
+}
diff --git a/openff/toolkit/topology/molecule.py b/openff/toolkit/topology/molecule.py
index 4d963e99..f0229c4b 100644
--- a/openff/toolkit/topology/molecule.py
+++ b/openff/toolkit/topology/molecule.py
@@ -690,11 +690,21 @@ class Bond(Serializable):
     @classmethod
     def from_dict(cls, molecule, d):
         """Create a Bond from a dict representation."""
-        # TODO
-        d["molecule"] = molecule
+        # TODO: This is not used anywhere (`Molecule._initialize_bonds_from_dict()` just calls grabs
+        #       the two atoms and calls `Molecule._add_bond`). Remove or change that?
+        # TODO: There is no point in feeding in a `molecule` argument since `Bond.__init__` already
+        #       requires (and checks) that the two atoms are part of the same molecule
         d["atom1"] = molecule.atoms[d["atom1"]]
         d["atom2"] = molecule.atoms[d["atom2"]]
-        return cls(*d)
+
+        return cls(
+            atom1=d["atom1"],
+            atom2=d["atom2"],
+            bond_order=d["bond_order"],
+            is_aromatic=d["is_aromatic"],
+            stereochemistry=d["stereochemistry"],
+            fractional_bond_order=d["fractional_bond_order"],
+        )
 
     @property
     def atom1(self):
@@ -758,7 +768,12 @@ class Bond(Serializable):
         """
         Sets the Bond's parent molecule. Can not be changed after assignment
         """
-        assert self._molecule is None
+        # TODO: This is an impossible state (the constructor requires that atom1 and atom2
+        #       are in a molecule, the same molecule, and sets that as self._molecule).
+        #       Should we remove this?
+        assert (
+            self._molecule is None
+        ), "Bond.molecule is already set and can only be set once"
         self._molecule = value
 
     @property
@@ -768,6 +783,7 @@ class Bond(Serializable):
 
         """
         if self._molecule is None:
+            # TODO: This is unreachable; see `Bond.molecule` setter
             raise ValueError("This Atom does not belong to a Molecule object")
         return self._molecule.bonds.index(self)
 
@@ -1108,7 +1124,20 @@ class FrozenMolecule(Serializable):
         """
         from openff.toolkit.utils.utils import serialize_numpy
 
-        molecule_dict = dict()
+        # typing.TypedDict might make this cleaner
+        # https://mypy.readthedocs.io/en/latest/typed_dict.html#typeddict
+        molecule_dict: Dict[
+            str,
+            Union[
+                None,
+                str,
+                bytes,
+                Dict[str, Any],
+                List[str],
+                List[bytes],
+                List[HierarchyElement],
+            ],
+        ] = dict()
         molecule_dict["name"] = self._name
 
         molecule_dict["atoms"] = [atom.to_dict() for atom in self._atoms]
@@ -1140,7 +1169,7 @@ class FrozenMolecule(Serializable):
 
         molecule_dict["hierarchy_schemes"] = dict()
         for iter_name, hier_scheme in self._hierarchy_schemes.items():
-            molecule_dict["hierarchy_schemes"][iter_name] = hier_scheme.to_dict()
+            molecule_dict["hierarchy_schemes"][iter_name] = hier_scheme.to_dict()  # type: ignore[index]
 
         return molecule_dict
 
@@ -1597,6 +1626,7 @@ class FrozenMolecule(Serializable):
         inchi: str,
         allow_undefined_stereo: bool = False,
         toolkit_registry: TKR = GLOBAL_TOOLKIT_REGISTRY,
+        name: str = "",
     ):
         """
         Construct a Molecule from a InChI representation
@@ -1615,6 +1645,8 @@ class FrozenMolecule(Serializable):
             or openff.toolkit.utils.toolkits.ToolkitWrapper, optional, default=None
             :class:`ToolkitRegistry` or :class:`ToolkitWrapper` to use for InChI-to-molecule conversion
 
+        name : str, default=""
+            An optional name for the output molecule
 
         Returns
         -------
@@ -1633,11 +1665,15 @@ class FrozenMolecule(Serializable):
                 inchi,
                 _cls=cls,
                 allow_undefined_stereo=allow_undefined_stereo,
+                name=name,
             )
         elif isinstance(toolkit_registry, ToolkitWrapper):
             toolkit = toolkit_registry
             molecule = toolkit.from_inchi(  # type: ignore[attr-defined]
-                inchi, _cls=cls, allow_undefined_stereo=allow_undefined_stereo
+                inchi,
+                _cls=cls,
+                allow_undefined_stereo=allow_undefined_stereo,
+                name=name,
             )
         else:
             raise InvalidToolkitRegistryError(
@@ -1750,6 +1786,7 @@ class FrozenMolecule(Serializable):
         hydrogens_are_explicit: bool = False,
         toolkit_registry: TKR = GLOBAL_TOOLKIT_REGISTRY,
         allow_undefined_stereo: bool = False,
+        name: str = "",
     ) -> "Molecule":
         """
         Construct a ``Molecule`` from a SMILES representation
@@ -1778,6 +1815,9 @@ class FrozenMolecule(Serializable):
             Whether to accept SMILES with undefined stereochemistry. If
             ``False``, an exception will be raised if a SMILES with undefined
             stereochemistry is passed into this function.
+        name : str, default=""
+            An optional name for the output molecule
+
 
         Raises
         ------
@@ -1813,6 +1853,7 @@ class FrozenMolecule(Serializable):
                 hydrogens_are_explicit=hydrogens_are_explicit,
                 allow_undefined_stereo=allow_undefined_stereo,
                 _cls=cls,
+                name=name,
             )
         elif isinstance(toolkit_registry, ToolkitWrapper):
             toolkit = toolkit_registry
@@ -1821,6 +1862,7 @@ class FrozenMolecule(Serializable):
                 hydrogens_are_explicit=hydrogens_are_explicit,
                 allow_undefined_stereo=allow_undefined_stereo,
                 _cls=cls,
+                name=name,
             )
         else:
             raise InvalidToolkitRegistryError(
@@ -3770,6 +3812,7 @@ class FrozenMolecule(Serializable):
                         "is likely to be lost. PDBs can be used along with a valid smiles string with RDKit using "
                         "the constructor Molecule.from_pdb_and_smiles(file_path, smiles)"
                     )
+
                 raise NotImplementedError(msg)
 
         elif isinstance(toolkit_registry, ToolkitWrapper):
@@ -3822,6 +3865,7 @@ class FrozenMolecule(Serializable):
         cls,
         file_path: Union[str, TextIO],
         toolkit_registry=GLOBAL_TOOLKIT_REGISTRY,
+        name: str = "",
     ):
         """
         Loads a polymer from a PDB file.
@@ -3854,6 +3898,8 @@ class FrozenMolecule(Serializable):
             PDB information to be passed to OpenMM PDBFile object for loading
         toolkit_registry = ToolkitWrapper or ToolkitRegistry. Default = None
             Either a ToolkitRegistry, ToolkitWrapper
+        name : str, default=""
+            An optional name for the output molecule
 
         Returns
         -------
@@ -3920,6 +3966,8 @@ class FrozenMolecule(Serializable):
                 + "load any small molecules with Molecule.from_pdb_and_smiles."
             )
 
+        offmol.name = name
+
         return offmol
 
     def _has_multiple_molecules(self) -> bool:
@@ -4174,7 +4222,10 @@ class FrozenMolecule(Serializable):
     @classmethod
     @RDKitToolkitWrapper.requires_toolkit()
     def from_rdkit(
-        cls, rdmol, allow_undefined_stereo=False, hydrogens_are_explicit=False
+        cls,
+        rdmol,
+        allow_undefined_stereo=False,
+        hydrogens_are_explicit=False,
     ):
         """
         Create a Molecule from an RDKit molecule.
@@ -4641,7 +4692,9 @@ class FrozenMolecule(Serializable):
 
     @classmethod
     @RDKitToolkitWrapper.requires_toolkit()
-    def from_pdb_and_smiles(cls, file_path, smiles, allow_undefined_stereo=False):
+    def from_pdb_and_smiles(
+        cls, file_path, smiles, allow_undefined_stereo=False, name=""
+    ):
         """
         Create a Molecule from a pdb file and a SMILES string using RDKit.
 
@@ -4665,6 +4718,8 @@ class FrozenMolecule(Serializable):
             a valid smiles string for the pdb, used for stereochemistry, formal charges, and bond order
         allow_undefined_stereo : bool, default=False
             If false, raises an exception if SMILES contains undefined stereochemistry.
+        name : str, default=""
+            An optional name for the output molecule
 
         Returns
         --------
@@ -4679,7 +4734,7 @@ class FrozenMolecule(Serializable):
 
         toolkit = RDKitToolkitWrapper()
         return toolkit.from_pdb_and_smiles(
-            file_path, smiles, allow_undefined_stereo, _cls=cls
+            file_path, smiles, allow_undefined_stereo, _cls=cls, name=name
         )
 
     def canonical_order_atoms(self, toolkit_registry=GLOBAL_TOOLKIT_REGISTRY):
@@ -5115,63 +5170,7 @@ class Molecule(FrozenMolecule):
 
     def __init__(self, *args, **kwargs):
         """
-        Create a new Molecule object
-
-        Parameters
-        ----------
-        other : optional, default=None
-            If specified, attempt to construct a copy of the molecule from the
-            specified object. This can be any one of the following:
-
-            * a :class:`Molecule` object
-            * a file that can be used to construct a :class:`Molecule` object
-            * an ``openeye.oechem.OEMol``
-            * an ``rdkit.Chem.rdchem.Mol``
-            * a serialized :class:`Molecule` object
-
-        Examples
-        --------
-
-        Create an empty molecule:
-
-        >>> empty_molecule = Molecule()
-
-        Create a molecule from a file that can be used to construct a molecule,
-        using either a filename or file-like object:
-
-        >>> from openff.toolkit.utils import get_data_file_path
-        >>> sdf_filepath = get_data_file_path('molecules/ethanol.sdf')
-        >>> molecule = Molecule(sdf_filepath)
-        >>> molecule = Molecule(open(sdf_filepath, 'r'), file_format='sdf')
-
-        >>> import gzip
-        >>> mol2_gz_filepath = get_data_file_path('molecules/toluene.mol2.gz')
-        >>> molecule = Molecule(gzip.GzipFile(mol2_gz_filepath, 'r'), file_format='mol2')
-
-        Create a molecule from another molecule:
-
-        >>> molecule_copy = Molecule(molecule)
-
-        Convert to OpenEye OEMol object
-
-        >>> oemol = molecule.to_openeye()
-
-        Create a molecule from an OpenEye molecule:
-
-        >>> molecule = Molecule(oemol)
-
-        Convert to RDKit Mol object
-
-        >>> rdmol = molecule.to_rdkit()
-
-        Create a molecule from an RDKit molecule:
-
-        >>> molecule = Molecule(rdmol)
-
-        Convert the molecule into a dictionary and back again:
-
-        >>> serialized_molecule = molecule.to_dict()
-        >>> molecule_copy = Molecule(serialized_molecule)
+        See FrozenMolecule.__init__
 
         .. todo ::
 
diff --git a/openff/toolkit/topology/topology.py b/openff/toolkit/topology/topology.py
index f3a5b151..5cc304fa 100644
--- a/openff/toolkit/topology/topology.py
+++ b/openff/toolkit/topology/topology.py
@@ -1695,6 +1695,7 @@ class Topology(Serializable):
             off_atom.metadata["residue_number"] = atom.residue.id
             off_atom.metadata["insertion_code"] = atom.residue.insertionCode
             off_atom.metadata["chain_id"] = atom.residue.chain.id
+            off_atom.name = atom.name
 
         for offmol in topology.molecules:
             offmol.add_default_hierarchy_schemes()
diff --git a/openff/toolkit/utils/openeye_wrapper.py b/openff/toolkit/utils/openeye_wrapper.py
index ad2cc16e..2194f0e8 100644
--- a/openff/toolkit/utils/openeye_wrapper.py
+++ b/openff/toolkit/utils/openeye_wrapper.py
@@ -216,7 +216,10 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
         return cls._is_available  # type: ignore[return-value]
 
     def from_object(
-        self, obj, allow_undefined_stereo: bool = False, _cls=None
+        self,
+        obj,
+        allow_undefined_stereo: bool = False,
+        _cls=None,
     ) -> "Molecule":
         """
         Convert an OEMol (or OEMol-derived object) into an openff.toolkit.topology.molecule
@@ -231,6 +234,7 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
             is passed into this function.
         _cls : class
             Molecule constructor
+
         Returns
         -------
         Molecule
@@ -251,7 +255,9 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
 
         if isinstance(obj, oechem.OEMolBase):
             return self.from_openeye(
-                oemol=obj, allow_undefined_stereo=allow_undefined_stereo, _cls=_cls
+                oemol=obj,
+                allow_undefined_stereo=allow_undefined_stereo,
+                _cls=_cls,
             )
         raise NotImplementedError(
             "Cannot create Molecule from {} object".format(type(obj))
@@ -1064,7 +1070,9 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
 
     @staticmethod
     def from_openeye(
-        oemol, allow_undefined_stereo: bool = False, _cls=None
+        oemol,
+        allow_undefined_stereo: bool = False,
+        _cls=None,
     ) -> "Molecule":
         """
         Create a Molecule from an OpenEye molecule. If the OpenEye molecule has
@@ -1193,7 +1201,10 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
             _cls = Molecule
 
         molecule = _cls()
-        molecule.name = oemol.GetTitle()
+
+        # OEMol.GetTitle() happens to default to an empty string
+        if oemol.GetTitle() != "":
+            molecule.name = oemol.GetTitle()
 
         # Copy any attached SD tag information
         for dp in oechem.OEGetSDDataPairs(oemol):
@@ -1953,6 +1964,7 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
         hydrogens_are_explicit: bool = False,
         allow_undefined_stereo: bool = False,
         _cls=None,
+        name: str = "",
     ) -> "Molecule":
         """
         Create a Molecule from a SMILES string using the OpenEye toolkit.
@@ -1971,6 +1983,8 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
             is passed into this function.
         _cls : class
             Molecule constructor
+        name : str, default=""
+            An optional name for the output molecule
 
         Returns
         -------
@@ -2006,12 +2020,19 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
             atom.SetPartialCharge(float("nan"))
 
         molecule = self.from_openeye(
-            oemol, _cls=_cls, allow_undefined_stereo=allow_undefined_stereo
+            oemol,
+            _cls=_cls,
+            allow_undefined_stereo=allow_undefined_stereo,
         )
+        molecule.name = name
         return molecule
 
     def from_inchi(
-        self, inchi: str, allow_undefined_stereo: bool = False, _cls=None
+        self,
+        inchi: str,
+        allow_undefined_stereo: bool = False,
+        _cls=None,
+        name: str = "",
     ) -> "Molecule":
         """
         Construct a Molecule from a InChI representation
@@ -2020,14 +2041,14 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
         ----------
         inchi : str
             The InChI representation of the molecule.
-
         allow_undefined_stereo : bool, default=False
             Whether to accept InChI with undefined stereochemistry. If False,
             an exception will be raised if a InChI with undefined stereochemistry
             is passed into this function.
-
         _cls : class
             Molecule constructor
+        name : str, default=""
+            An optional name for the output molecule
 
         Returns
         -------
@@ -2048,8 +2069,11 @@ class OpenEyeToolkitWrapper(base_wrapper.ToolkitWrapper):
             )
 
         molecule = self.from_openeye(
-            oemol, allow_undefined_stereo=allow_undefined_stereo, _cls=_cls
+            oemol,
+            allow_undefined_stereo=allow_undefined_stereo,
+            _cls=_cls,
         )
+        molecule.name = name
 
         return molecule
 
diff --git a/openff/toolkit/utils/rdkit_wrapper.py b/openff/toolkit/utils/rdkit_wrapper.py
index 88bb475c..c4bae64e 100644
--- a/openff/toolkit/utils/rdkit_wrapper.py
+++ b/openff/toolkit/utils/rdkit_wrapper.py
@@ -125,7 +125,12 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
                 cls._is_available = True
         return cls._is_available
 
-    def from_object(self, obj, allow_undefined_stereo: bool = False, _cls=None):
+    def from_object(
+        self,
+        obj,
+        allow_undefined_stereo: bool = False,
+        _cls=None,
+    ):
         """
         If given an rdchem.Mol (or rdchem.Mol-derived object), this function will load it into an
         openff.toolkit.topology.molecule. Otherwise, it will return False.
@@ -159,7 +164,10 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
 
             _cls = Molecule
         if isinstance(obj, Chem.rdchem.Mol):
-            return _cls.from_rdkit(obj, allow_undefined_stereo=allow_undefined_stereo)
+            return _cls.from_rdkit(
+                obj,
+                allow_undefined_stereo=allow_undefined_stereo,
+            )
         raise NotImplementedError(
             "Cannot create Molecule from {} object".format(type(obj))
         )
@@ -170,6 +178,7 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
         smiles: str,
         allow_undefined_stereo: bool = False,
         _cls=None,
+        name: str = "",
     ):
         """
         Create a Molecule from a pdb file and a SMILES string using RDKit.
@@ -194,6 +203,8 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
             If false, raises an exception if SMILES contains undefined stereochemistry.
         _cls : class
             Molecule constructor
+        name : str, default=""
+            An optional name for the output molecule
 
         Returns
         --------
@@ -209,7 +220,9 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
 
         # Make the molecule from smiles
         offmol = self.from_smiles(
-            smiles, allow_undefined_stereo=allow_undefined_stereo, _cls=_cls
+            smiles,
+            allow_undefined_stereo=allow_undefined_stereo,
+            _cls=_cls,
         )
 
         # Make another molecule from the PDB. We squelch stereo errors here, since
@@ -254,6 +267,7 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
             newatom.name = pdbatom.name
         new_mol.add_default_hierarchy_schemes()
 
+        new_mol.name = name
         return new_mol
 
     def _polymer_openmm_topology_to_offmol(
@@ -316,6 +330,7 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
             top._add_molecule_keep_cache(offmol)
         if pdbfile.topology.getPeriodicBoxVectors() is not None:
             top.box_vectors = from_openmm(pdbfile.topology.getPeriodicBoxVectors())
+        top.set_positions(coords_angstrom * unit.angstrom)
 
         return top
 
@@ -1018,6 +1033,7 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
         hydrogens_are_explicit: bool = False,
         allow_undefined_stereo: bool = False,
         _cls=None,
+        name: str = "",
     ):
         """
         Create a Molecule from a SMILES string using the RDKit toolkit.
@@ -1036,6 +1052,8 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
             is passed into this function.
         _cls : class
             Molecule constructor
+        name : str, default=""
+            An optional name to pass to the _cls constructor
 
         Returns
         -------
@@ -1100,10 +1118,16 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
             allow_undefined_stereo=allow_undefined_stereo,
             hydrogens_are_explicit=hydrogens_are_explicit,
         )
-
+        molecule.name = name
         return molecule
 
-    def from_inchi(self, inchi: str, allow_undefined_stereo: bool = False, _cls=None):
+    def from_inchi(
+        self,
+        inchi: str,
+        allow_undefined_stereo: bool = False,
+        _cls=None,
+        name: str = "",
+    ):
         """
         Construct a Molecule from a InChI representation
 
@@ -1151,6 +1175,7 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
         molecule = self.from_rdkit(
             rdmol, allow_undefined_stereo=allow_undefined_stereo, _cls=_cls
         )
+        molecule.name = name
 
         return molecule
 
@@ -1783,12 +1808,9 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
         # Create a new OpenFF Molecule
         offmol = _cls()
 
-        # If RDMol has a title save it
+        # If RDMol has a title, use it
         if rdmol.HasProp("_Name"):
-            # raise Exception('{}'.format(rdmol.GetProp('name')))
             offmol.name = rdmol.GetProp("_Name")
-        else:
-            offmol.name = ""
 
         # Store all properties
         # TODO: Should there be an API point for storing properties?
@@ -2163,7 +2185,7 @@ class RDKitToolkitWrapper(base_wrapper.ToolkitWrapper):
         rdmol = Chem.RWMol(rdmol)
         # Set name
         # TODO: What is the best practice for how this should be named?
-        if not (molecule.name is None):
+        if molecule.name != "":
             rdmol.SetProp("_Name", molecule.name)
 
         # TODO: Set other properties
diff --git a/setup.cfg b/setup.cfg
index 4433012e..8997f3e2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,8 +24,6 @@ per-file-ignores =
     openff/toolkit/*/__init__.py:F401
     openff/toolkit/topology/molecule.py
     openff/toolkit/topology/topology.py:F821
-    openff/toolkit/utils/openeye_wrapper.py
-    openff/toolkit/utils/rdkit_wrapper.py
     openff/toolkit/utils/ambertools_wrapper.py:W293
     openff/toolkit/tests/test_molecule.py
     openff/toolkit/tests/test_forcefield.py:E501


Test patch:
diff --git a/openff/toolkit/tests/test_forcefield.py b/openff/toolkit/tests/test_forcefield.py
index 1517b5a8..7eab9b8d 100644
--- a/openff/toolkit/tests/test_forcefield.py
+++ b/openff/toolkit/tests/test_forcefield.py
@@ -3680,6 +3680,7 @@ class TestForceFieldParameterAssignment(_ForceFieldFixtures):
         #     modify_system=False,
         # )
 
+    @pytest.mark.slow
     @requires_openeye_mol2
     @pytest.mark.parametrize("zero_charges", [True, False])
     @pytest.mark.parametrize(("gbsa_model"), ["HCT", "OBC1", "OBC2"])
diff --git a/openff/toolkit/tests/test_molecule.py b/openff/toolkit/tests/test_molecule.py
index 6b7de645..9189e021 100644
--- a/openff/toolkit/tests/test_molecule.py
+++ b/openff/toolkit/tests/test_molecule.py
@@ -41,6 +41,7 @@ from openff.toolkit.tests.utils import (
 )
 from openff.toolkit.topology.molecule import (
     Atom,
+    Bond,
     BondExistsError,
     FrozenMolecule,
     HierarchyElement,
@@ -426,11 +427,33 @@ class TestAtom:
 
 
 class TestBond:
-    def test_float_bond_order(self):
+    @pytest.fixture()
+    def bond(self):
+        return create_ethanol().bond(0)
+
+    def test_cannot_change_molecule(self, bond):
+        with pytest.raises(
+            AssertionError,
+            match="Bond.molecule is already set and can only be set once",
+        ):
+            bond.molecule = create_reversed_ethanol()
+
+    def test_initialize_from_dict(self):
         molecule = create_ethanol()
 
+        bond = Bond.from_dict(molecule=molecule, d=molecule.bond(0).to_dict())
+
+        assert bond.atom1_index == 0
+        assert bond.atom2_index == 1
+        assert bond.atom1.atomic_number == 6
+        assert bond.atom2.atomic_number == 6
+
+    def test_set_bond_order(self, bond):
+        bond.bond_order = 2
+
+    def test_float_bond_order(self, bond):
         with pytest.raises(InvalidBondOrderError):
-            molecule.bond(0).bond_order = 1.2
+            bond.bond_order = 1.2
 
 
 class TestMolecule:
@@ -638,6 +661,14 @@ class TestMolecule:
         smiles2 = molecule2.to_smiles(toolkit_registry=toolkit_wrapper)
         assert smiles1 == smiles2
 
+    def test_from_smiles_name(self):
+        """Test name kwarg to from_smiles"""
+        mol = Molecule.from_smiles("C")
+        assert mol.name == ""
+
+        mol = Molecule.from_smiles("C", name="bob")
+        assert mol.name == "bob"
+
     @pytest.mark.parametrize(
         "smiles, expected", [("[Cl:1]Cl", {0: 1}), ("[Cl:1][Cl:2]", {0: 1, 1: 2})]
     )
@@ -942,6 +973,13 @@ class TestMolecule:
 
         compare_mols(ref_mol, nonstandard_inchi_mol)
 
+    def test_from_inchi_name(self):
+        """Test name kwarg to from_inchi"""
+        mol = Molecule.from_inchi("InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3")
+        assert mol.name == ""
+        mol = Molecule.from_inchi("InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3", name="bob")
+        assert mol.name == "bob"
+
     # TODO: Should there be an equivalent toolkit test and leave this as an integration test?
     @requires_openeye
     def test_create_from_file(self):
@@ -2416,6 +2454,15 @@ class TestMolecule:
 
             assert pdb_mol.is_isomorphic_with(smiles_mol)
 
+        def test_name_kwarg(self, pdb_path, smiles, sdf_path):
+            """Ensure the name kwarg is wired up correctly"""
+            pdb_path = get_data_file_path(pdb_path)
+            pdb_mol = Molecule.from_pdb_and_smiles(pdb_path, smiles)
+            assert pdb_mol.name == ""
+
+            pdb_mol = Molecule.from_pdb_and_smiles(pdb_path, smiles, name="bob")
+            assert pdb_mol.name == "bob"
+
         def test_matches_sdf(self, pdb_path, smiles, sdf_path):
             """The produced Molecule should exactly match the corresponding SDF
 
@@ -3877,6 +3924,17 @@ class TestMoleculeFromPDB:
             atol=0.01,
         )
 
+    def test_molecule_from_pdb_name(self):
+        offmol = Molecule.from_polymer_pdb(
+            get_data_file_path("proteins/MainChain_ALA.pdb")
+        )
+        assert offmol.name == ""
+
+        offmol = Molecule.from_polymer_pdb(
+            get_data_file_path("proteins/MainChain_ALA.pdb"), name="bob"
+        )
+        assert offmol.name == "bob"
+
     def test_molecule_from_pdb_mainchain_ala_tripeptide(self):
         offmol = Molecule.from_polymer_pdb(
             get_data_file_path("proteins/MainChain_ALA_ALA.pdb")
diff --git a/openff/toolkit/tests/test_toolkits.py b/openff/toolkit/tests/test_toolkits.py
index cb41629b..f982c084 100644
--- a/openff/toolkit/tests/test_toolkits.py
+++ b/openff/toolkit/tests/test_toolkits.py
@@ -707,6 +707,16 @@ class TestOpenEyeToolkitWrapper:
         molecule_from_expl = Molecule.from_openeye(oemol_expl)
         assert molecule_from_expl.to_smiles() == molecule_from_impl.to_smiles()
 
+    def test_openeye_from_smiles_name(self):
+        """
+        Test to ensure that the `name` kwarg to from_smiles behaves correctly
+        """
+        toolkit_wrapper = OpenEyeToolkitWrapper()
+        mol = toolkit_wrapper.from_smiles("C")
+        assert mol.name == ""
+        mol = toolkit_wrapper.from_smiles("C", name="bob")
+        assert mol.name == "bob"
+
     def test_openeye_from_smiles_hydrogens_are_explicit(self):
         """
         Test to ensure that OpenEyeToolkitWrapper.from_smiles has the proper behavior with
@@ -774,6 +784,18 @@ class TestOpenEyeToolkitWrapper:
         molecule.to_inchikey(toolkit_registry=toolkit)
         molecule.to_inchikey(True, toolkit_registry=toolkit)
 
+    def test_openeye_from_inchi_name(self):
+        """
+        Test to ensure that the `name` kwarg to from_inchi behaves correctly
+        """
+        toolkit_wrapper = OpenEyeToolkitWrapper()
+        mol = toolkit_wrapper.from_inchi("InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3")
+        assert mol.name == ""
+        mol = toolkit_wrapper.from_inchi(
+            "InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3", name="bob"
+        )
+        assert mol.name == "bob"
+
     def test_from_bad_inchi(self):
         """Test building a molecule from a bad InChI string"""
 
@@ -2043,6 +2065,16 @@ class TestRDKitToolkitWrapper:
         smiles2 = molecule.to_smiles(toolkit_registry=toolkit_wrapper)
         assert smiles2 == expected_output_smiles
 
+    def test_rdkit_from_smiles_name(self):
+        """
+        Test to ensure that the `name` kwarg to from_smiles behaves correctly
+        """
+        toolkit_wrapper = RDKitToolkitWrapper()
+        mol = toolkit_wrapper.from_smiles("C")
+        assert mol.name == ""
+        mol = toolkit_wrapper.from_smiles("C", name="bob")
+        assert mol.name == "bob"
+
     def test_rdkit_from_smiles_radical(self):
         """Test that parsing an SMILES with a radical raises RadicalsNotSupportedError."""
         with pytest.raises(RadicalsNotSupportedError):
@@ -2167,6 +2199,18 @@ class TestRDKitToolkitWrapper:
 
         compare_mols(ref_mol, nonstandard_inchi_mol)
 
+    def test_rdkit_from_inchi_name(self):
+        """
+        Test to ensure that the `name` kwarg to from_inchi behaves correctly
+        """
+        toolkit_wrapper = RDKitToolkitWrapper()
+        mol = toolkit_wrapper.from_inchi("InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3")
+        assert mol.name == ""
+        mol = toolkit_wrapper.from_inchi(
+            "InChI=1S/C2H6O/c1-2-3/h3H,2H2,1H3", name="bob"
+        )
+        assert mol.name == "bob"
+
     @pytest.mark.parametrize("molecule", get_mini_drug_bank(RDKitToolkitWrapper))
     def test_non_standard_inchi_round_trip(self, molecule):
         """Test if a molecule can survive an InChi round trip test in some cases the standard InChI