Repository: pckroon/pysmiles

Issue:
Inconsistent writing and reading of mono-atomic smiles for Se and As
If we have a graph featuring a single non-organic atom like Se, this will be outputted by the smiles writer as 'Se'. According to smiles rules, I would have expected '[Se]'. And then, if we again read in the smile 'Se' with the smiles reader, it will fail and output a graph featuring 'S', because it relies on the brackets to recognise non-organic elements.
I don't know if this is an issue with the smiles reader or writer, but they are inconsistent. 

Comments:

Patch:
diff --git a/pysmiles/read_smiles.py b/pysmiles/read_smiles.py
index f4bf6e0..79e9634 100644
--- a/pysmiles/read_smiles.py
+++ b/pysmiles/read_smiles.py
@@ -103,9 +103,12 @@ def read_smiles(smiles, explicit_hydrogen=False, zero_order_bonds=True,
         The SMILES string to parse. Should conform to the OpenSMILES
         specification.
     explicit_hydrogen : bool
-        Whether hydrogens should be explicit nodes in the outout graph, or be
+        Whether hydrogens should be explicit nodes in the output graph, or be
         implicit in 'hcount' attributes.
-    reinterprit_aromatic : bool
+    zero_order_bonds : bool
+        Whether zero-order bonds (".") should be added as edges with an order of
+        0.
+    reinterpret_aromatic : bool
         Whether aromaticity should be determined from the created molecule,
         instead of taken from the SMILES string.
 
diff --git a/pysmiles/smiles_helper.py b/pysmiles/smiles_helper.py
index 86b05e3..0c13c19 100644
--- a/pysmiles/smiles_helper.py
+++ b/pysmiles/smiles_helper.py
@@ -141,8 +141,8 @@ def format_atom(molecule, node_key, default_element='*'):
     if aromatic:
         name = name.lower()
 
-    if (stereo is None and isotope == '' and charge == 0 and default_h and
-            class_ == '' and name.lower() in 'b c n o p s se as *'.split()):
+    if (stereo is None and isotope == '' and charge == 0 and default_h and class_ == '' and
+            (name.lower() in 'b c n o p s *'.split() or name in 'F Cl Br I'.split())):
         return name
 
     if hcount:
diff --git a/pysmiles/write_smiles.py b/pysmiles/write_smiles.py
index ea869dc..4263701 100644
--- a/pysmiles/write_smiles.py
+++ b/pysmiles/write_smiles.py
@@ -18,11 +18,13 @@ Exposes functionality for writing SMILES strings
 """
 
 from collections import defaultdict
+import logging
 
 import networkx as nx
 
 from .smiles_helper import remove_explicit_hydrogens, format_atom
 
+LOGGER = logging.getLogger(__name__)
 
 def _get_ring_marker(used_markers):
     """
@@ -74,10 +76,10 @@ def _write_edge_symbol(molecule, n_idx, n_jdx):
     return cross_aromatic or not (aromatic_bond or single_bond)
 
 
-def write_smiles(molecule, default_element='*', start=None):
+def write_smiles_component(molecule, default_element='*', start=None):
     """
     Creates a SMILES string describing `molecule` according to the OpenSMILES
-    standard.
+    standard. `molecule` should be a single connected component.
 
     Parameters
     ----------
@@ -187,3 +189,43 @@ def write_smiles(molecule, default_element='*', start=None):
 
     smiles += ')' * branch_depth
     return smiles
+
+def write_smiles(molecule, default_element='*', start=None):
+    """
+    Creates a SMILES string describing `molecule` according to the OpenSMILES
+    standard. If `molecule` consists of multiple disconnected components their
+    corresponding SMILES string will be joined by zero-order bonds (".").
+
+    Parameters
+    ----------
+    molecule : nx.Graph
+        The molecule for which a SMILES string should be generated.
+    default_element : str
+        The element to write if the attribute is missing for a node.
+    start : Hashable
+        The atom at which the depth first traversal of the molecule should
+        start. A sensible one is chosen: preferably a terminal heteroatom.
+
+    Returns
+    -------
+    str
+        The SMILES string describing `molecule`.
+    """
+    smiles = []
+    components = list(nx.connected_components(molecule))
+    try:
+        components = sorted(components, key=lambda c: sorted(c))
+    except TypeError:
+        pass
+    for nodes in components:
+        if start is not None and start in nodes:
+            start_ = start
+        else:
+            start_ = None
+        smiles.append(write_smiles_component(molecule.subgraph(nodes),
+                      default_element=default_element, start=start_))
+    if len(smiles) > 1:
+        LOGGER.info('Be aware the specified molecule is disconnected and '
+                    'consists of %d connected components.', len(smiles))
+    return '.'.join(smiles)
+


Test patch:
diff --git a/tests/test_write_smiles.py b/tests/test_write_smiles.py
index 1b5fa97..373b037 100644
--- a/tests/test_write_smiles.py
+++ b/tests/test_write_smiles.py
@@ -19,7 +19,7 @@ from pysmiles import write_smiles, read_smiles
 from pysmiles.testhelper import assertEqualGraphs, make_mol
 
 
-@pytest.mark.parametrize('node_data, edge_data, expl_h', (
+@pytest.mark.parametrize('node_data, edge_data, kwargs', (
     (
         [(0, {'element': 'C', 'charge': 0, 'aromatic': False, 'hcount': 3}),
          (1, {'element': 'C', 'charge': 0, 'aromatic': False, 'hcount': 2}),
@@ -28,7 +28,7 @@ from pysmiles.testhelper import assertEqualGraphs, make_mol
         [(0, 1, {'order': 1}),
          (1, 2, {'order': 1}),
          (2, 3, {'order': 1})],
-        False
+        dict(explicit_hydrogen=False, zero_order_bonds=True, reinterpret_aromatic=True),
     ),
     (
         [(0, {'element': 'C', 'charge': 0, 'aromatic': False, 'hcount': 3}),
@@ -38,7 +38,7 @@ from pysmiles.testhelper import assertEqualGraphs, make_mol
         [(0, 1, {'order': 1}),
          (1, 2, {'order': 1}),
          (1, 3, {'order': 1})],
-        False
+        dict(explicit_hydrogen=False, zero_order_bonds=True, reinterpret_aromatic=True),
     ),
     (
         [(0, {'element': 'C', 'charge': 0, 'aromatic': False, 'hcount': 3}),
@@ -50,7 +50,7 @@ from pysmiles.testhelper import assertEqualGraphs, make_mol
          (1, 2, {'order': 1}),
          (2, 3, {'order': 1}),
          (3, 4, {'order': 2})],
-        False
+        dict(explicit_hydrogen=False, zero_order_bonds=True, reinterpret_aromatic=True),
     ),
     (
         [(0, {'element': 'C', 'charge': 0, 'aromatic': True, 'hcount': 1}),
@@ -67,7 +67,7 @@ from pysmiles.testhelper import assertEqualGraphs, make_mol
          (4, 5, {'order': 1.5}),
          (5, 0, {'order': 1.5}),
          (3, 6, {'order': 1})],
-        False
+        dict(explicit_hydrogen=False, zero_order_bonds=True, reinterpret_aromatic=False),
     ),
     (
         [(0, {'element': 'C', 'charge': 0, 'aromatic': True, 'hcount': 1}),
@@ -84,7 +84,7 @@ from pysmiles.testhelper import assertEqualGraphs, make_mol
          (4, 5, {'order': 1.5}),
          (5, 0, {'order': 1.5}),
          (3, 6, {'order': 2})],
-        False
+        dict(explicit_hydrogen=False, zero_order_bonds=True, reinterpret_aromatic=False),
     ),
     (
         [(0, {'element': 'C', 'charge': 0, 'aromatic': True, 'hcount': 1}),
@@ -108,7 +108,7 @@ from pysmiles.testhelper import assertEqualGraphs, make_mol
          (8, 9, {'order': 1.5}),
          (9, 0, {'order': 1.5}),
          (2, 7, {'order': 1.5})],
-        False
+        dict(explicit_hydrogen=False, zero_order_bonds=True, reinterpret_aromatic=True),
     ),
     (
         [(0, {'element': 'C', 'charge': -1, 'aromatic': False, 'hcount': 3}),
@@ -118,7 +118,7 @@ from pysmiles.testhelper import assertEqualGraphs, make_mol
         [(0, 1, {'order': 1}),
          (1, 2, {'order': 1}),
          (2, 3, {'order': 1})],
-        False
+        dict(explicit_hydrogen=False, zero_order_bonds=True, reinterpret_aromatic=True),
     ),
     (
         [(0, {'element': 'C', 'charge': -1, 'aromatic': False, 'hcount': 3}),
@@ -128,11 +128,34 @@ from pysmiles.testhelper import assertEqualGraphs, make_mol
         [(0, 1, {'order': 1}),
          (1, 2, {'order': 1}),
          (2, 3, {'order': 1})],
-        False
+        dict(explicit_hydrogen=False, zero_order_bonds=True, reinterpret_aromatic=True),
+    ),
+    (
+        [(0, {'element': 'C', 'charge': 0, 'aromatic': False, 'hcount': 4}),
+         (1, {'element': 'C', 'charge': 0, 'aromatic': False, 'hcount': 4})],
+        [],
+        dict(explicit_hydrogen=False, zero_order_bonds=False, reinterpret_aromatic=True),
+    ),
+    (
+        [('a', {'element': 'C', 'charge': 0, 'aromatic': False, 'hcount': 4}),
+         (1, {'element': 'C', 'charge': 0, 'aromatic': False, 'hcount': 4})],
+        [],
+        dict(explicit_hydrogen=False, zero_order_bonds=False, reinterpret_aromatic=True),
+    ),
+    (
+      [(0, {'element': 'Se', 'charge': 0, 'aromatic': False, 'hcount': 0})],
+      [],
+      False
+    ),
+    (
+      [(0, {'element': 'As', 'charge': 0, 'aromatic': False, 'hcount': 0})],
+      [],
+      False
     ),
 ))
-def test_write_smiles(node_data, edge_data, expl_h):
+def test_write_smiles(node_data, edge_data, kwargs):
     mol = make_mol(node_data, edge_data)
     smiles = write_smiles(mol)
-    found = read_smiles(smiles, explicit_hydrogen=expl_h, reinterpret_aromatic=False)
+    print(smiles)
+    found = read_smiles(smiles, **kwargs)
     assertEqualGraphs(mol, found)