changeset 16:a638d8d13bb3 draft default tip

planemo upload for repository https://github.com/bgruening/galaxytools/tree/master/chemicaltoolbox/openbabel commit d9c51279c061a1da948a2582d5b502ca7573adbf
author bgruening
date Thu, 15 Aug 2024 11:02:57 +0000
parents 01ab890bf0ad
children
files change_title_to_metadata_value.py cheminfolib.py distance_finder.py multi_obgrep.py ob_addh.py ob_filter.py ob_genProp.py ob_remIons.py ob_spectrophore_search.py remove_protonation_state.py subsearch.py test-data/2_mol.sdf
diffstat 12 files changed, 566 insertions(+), 264 deletions(-) [+]
line wrap: on
line diff
--- a/change_title_to_metadata_value.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/change_title_to_metadata_value.py	Thu Aug 15 11:02:57 2024 +0000
@@ -11,6 +11,7 @@
 import string
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
@@ -19,14 +20,19 @@
         description="Change the title from a molecule file to metadata \
                      value of a given-id of the same molecule file.",
     )
-    parser.add_argument('--infile', '-i', required=True,
-                        help="path to the input file")
-    parser.add_argument('--outfile', '-o', required=True,
-                        help="path to the output file")
-    parser.add_argument('--key', '-k', required=True,
-                        help="the metadata key from the sdf file which should inlcude the new title")
-    parser.add_argument('--random', '-r', action="store_true",
-                        help="Add random suffix to the title.")
+    parser.add_argument("--infile", "-i", required=True, help="path to the input file")
+    parser.add_argument(
+        "--outfile", "-o", required=True, help="path to the output file"
+    )
+    parser.add_argument(
+        "--key",
+        "-k",
+        required=True,
+        help="the metadata key from the sdf file which should inlcude the new title",
+    )
+    parser.add_argument(
+        "--random", "-r", action="store_true", help="Add random suffix to the title."
+    )
 
     args = parser.parse_args()
 
@@ -35,8 +41,11 @@
         if args.key in mol.data:
             mol.title = mol.data[args.key]
             if args.random:
-                suffix = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(13))
-                mol.title += '__%s' % suffix
+                suffix = "".join(
+                    random.choice(string.ascii_lowercase + string.digits)
+                    for _ in range(13)
+                )
+                mol.title += "__%s" % suffix
         output.write(mol)
 
     output.close()
--- a/cheminfolib.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/cheminfolib.py	Thu Aug 15 11:02:57 2024 +0000
@@ -11,28 +11,32 @@
 import tempfile
 from multiprocessing import Pool
 
-
 try:
     from galaxy import eggs
-    eggs.require('psycopg2')
+
+    eggs.require("psycopg2")
 except ImportError:
     psycopg2 = None
-    print('psycopg2 is not available. It is currently used in the pgchem wrappers, that are not shipped with default CTB')
+    print(
+        "psycopg2 is not available. It is currently used in the pgchem wrappers, that are not shipped with default CTB"
+    )
 
 try:
     from openbabel import openbabel, pybel
+
     openbabel.obErrorLog.StopLogging()
 except ImportError:
     openbabel, pybel = None, None
-    print('OpenBabel could not be found. A few functions are not available without OpenBabel.')
+    print(
+        "OpenBabel could not be found. A few functions are not available without OpenBabel."
+    )
 
 
 def CountLines(path):
-    out = subprocess.Popen(['wc', '-l', path],
-                           stdout=subprocess.PIPE,
-                           stderr=subprocess.STDOUT
-                           ).communicate()[0]
-    return int(out.partition(b' ')[0])
+    out = subprocess.Popen(
+        ["wc", "-l", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+    ).communicate()[0]
+    return int(out.partition(b" ")[0])
 
 
 def grep(pattern, file_obj):
@@ -49,15 +53,15 @@
     for line_counter, line in enumerate(open(filepath)):
         if line_counter > 10000:
             break
-        if line.find('$$$$') != -1:
-            return 'sdf'
-        elif line.find('@<TRIPOS>MOLECULE') != -1:
-            return 'mol2'
-        elif line.find('ligand id') != -1:
-            return 'drf'
-        elif possible_inchi and re.findall('^InChI=', line):
-            return 'inchi'
-        elif re.findall(r'^M\s+END', line):
+        if line.find("$$$$") != -1:
+            return "sdf"
+        elif line.find("@<TRIPOS>MOLECULE") != -1:
+            return "mol2"
+        elif line.find("ligand id") != -1:
+            return "drf"
+        elif possible_inchi and re.findall("^InChI=", line):
+            return "inchi"
+        elif re.findall(r"^M\s+END", line):
             mol = True
         # first line is not an InChI, so it can't be an InChI file
         possible_inchi = False
@@ -65,99 +69,128 @@
     if mol:
         # END can occures before $$$$, so and SDF file will
         # be recognised as mol, if you not using this hack'
-        return 'mol'
-    return 'smi'
+        return "mol"
+    return "smi"
 
 
 def db_connect(args):
     try:
-        db_conn = psycopg2.connect("dbname=%s user=%s host=%s password=%s" % (args.dbname, args.dbuser, args.dbhost, args.dbpasswd))
+        db_conn = psycopg2.connect(
+            "dbname=%s user=%s host=%s password=%s"
+            % (args.dbname, args.dbuser, args.dbhost, args.dbpasswd)
+        )
         return db_conn
     except psycopg2.Error:
-        sys.exit('Unable to connect to the db')
+        sys.exit("Unable to connect to the db")
 
 
 ColumnNames = {
-    'can_smiles': 'Canonical SMILES',
-    'can': 'Canonical SMILES',
-    'inchi': 'InChI',
-    'inchi_key': 'InChI key',
-    'inchi_key_first': 'InChI key first',
-    'inchi_key_last': 'InChI key last',
-    'molwt': 'Molecular weight',
-    'hbd': 'Hydrogen-bond donors',
-    'donors': 'Hydrogen-bond donors',
-    'hba': 'Hydrogen-bond acceptors',
-    'acceptors': 'Hydrogen-bond acceptors',
-    'rotbonds': 'Rotatable bonds',
-    'logp': 'logP',
-    'psa': 'Polar surface area',
-    'mr': 'Molecular refractivity',
-    'atoms': 'Number of heavy atoms',
-    'rings': 'Number of rings',
-    'set_bits': 'FP2 bits',
-    'id': 'Internal identifier',
-    'tani': 'Tanimoto coefficient',
-    'spectrophore': 'Spectrophores(TM)',
-    'dist_spectrophore': 'Spectrophores(TM) distance to target',
-    'synonym': 'Entry id',
+    "can_smiles": "Canonical SMILES",
+    "can": "Canonical SMILES",
+    "inchi": "InChI",
+    "inchi_key": "InChI key",
+    "inchi_key_first": "InChI key first",
+    "inchi_key_last": "InChI key last",
+    "molwt": "Molecular weight",
+    "hbd": "Hydrogen-bond donors",
+    "donors": "Hydrogen-bond donors",
+    "hba": "Hydrogen-bond acceptors",
+    "acceptors": "Hydrogen-bond acceptors",
+    "rotbonds": "Rotatable bonds",
+    "logp": "logP",
+    "psa": "Polar surface area",
+    "mr": "Molecular refractivity",
+    "atoms": "Number of heavy atoms",
+    "rings": "Number of rings",
+    "set_bits": "FP2 bits",
+    "id": "Internal identifier",
+    "tani": "Tanimoto coefficient",
+    "spectrophore": "Spectrophores(TM)",
+    "dist_spectrophore": "Spectrophores(TM) distance to target",
+    "synonym": "Entry id",
 }
 
 OBDescriptor = {
-    'atoms': ["atoms", "Number of atoms"],
-    'hatoms': ["hatoms", "Number of heavy atoms"],  # self defined tag hatoms in plugindefines.txt
-    'can_smiles': ["cansmi", "Canonical SMILES"],
-    'can_smilesNS': ["cansmiNS", "Canonical SMILES without isotopes or stereo"],
+    "atoms": ["atoms", "Number of atoms"],
+    "hatoms": [
+        "hatoms",
+        "Number of heavy atoms",
+    ],  # self defined tag hatoms in plugindefines.txt
+    "can_smiles": ["cansmi", "Canonical SMILES"],
+    "can_smilesNS": ["cansmiNS", "Canonical SMILES without isotopes or stereo"],
     # ["abonds", "Number of aromatic bonds"],
     # ["bonds", "Number of bonds"],
     # ["dbonds", "Number of double bonds"],
     # ["formula", "Chemical formula"],
-    'hba': ["HBA1", "Number of Hydrogen Bond Acceptors 1 (JoelLib)"],
-    'hba2': ["HBA2", "Number of Hydrogen Bond Acceptors 2 (JoelLib)"],
-    'hbd': ["HBD", "Number of Hydrogen Bond Donors (JoelLib)"],
-    'inchi': ["InChI", "IUPAC InChI identifier"],
-    'inchi_key': ["InChIKey", "InChIKey"],
+    "hba": ["HBA1", "Number of Hydrogen Bond Acceptors 1 (JoelLib)"],
+    "hba2": ["HBA2", "Number of Hydrogen Bond Acceptors 2 (JoelLib)"],
+    "hbd": ["HBD", "Number of Hydrogen Bond Donors (JoelLib)"],
+    "inchi": ["InChI", "IUPAC InChI identifier"],
+    "inchi_key": ["InChIKey", "InChIKey"],
     # ["L5", "Lipinski Rule of Five"],
-    'logp': ["logP", "octanol/water partition coefficient"],
-    'mr': ["MR", "molar refractivity"],
-    'molwt': ["MW", "Molecular Weight filter"],
+    "logp": ["logP", "octanol/water partition coefficient"],
+    "mr": ["MR", "molar refractivity"],
+    "molwt": ["MW", "Molecular Weight filter"],
     # ["nF", "Number of Fluorine Atoms"],
     # ["s", "SMARTS filter"],
     # ["sbonds", "Number of single bonds"],
     # ["smarts", "SMARTS filter"],
     # ["tbonds", "Number of triple bonds"],
     # ["title", "For comparing a molecule's title"],
-    'psa': ["TPSA", "topological polar surface area"],
-    'rotbonds': ['ROTATABLE_BOND', 'rotatable bonds'],
+    "psa": ["TPSA", "topological polar surface area"],
+    "rotbonds": ["ROTATABLE_BOND", "rotatable bonds"],
 }
 
 
 def print_output(args, rows):
-    if args.oformat == 'table':
-        outfile = open(args.output, 'w')
-        requested_fields = (filter(lambda x: x not in ["[", "]", "'"], args.fetch)).split(', ')
+    if args.oformat == "table":
+        outfile = open(args.output, "w")
+        requested_fields = (
+            filter(lambda x: x not in ["[", "]", "'"], args.fetch)
+        ).split(", ")
         if args.header:
-            outfile.write('Identifier\t' + '\t'.join([ColumnNames[key] for key in requested_fields]) + '\n')
+            outfile.write(
+                "Identifier\t"
+                + "\t".join([ColumnNames[key] for key in requested_fields])
+                + "\n"
+            )
         for row in rows:
-            outfile.write(row['synonym'] + '\t' + '\t'.join([str(row[key]) for key in requested_fields]) + '\n')
+            outfile.write(
+                row["synonym"]
+                + "\t"
+                + "\t".join([str(row[key]) for key in requested_fields])
+                + "\n"
+            )
 
-    elif args.oformat in ['sdf', 'mol2']:
+    elif args.oformat in ["sdf", "mol2"]:
         outfile = pybel.Outputfile(args.oformat, args.output, overwrite=True)
         for row in rows:
             try:
-                mol = pybel.readstring('sdf', row['mol'])
-                if args.oformat == 'sdf':
-                    keys = filter(lambda x: x not in ["[", "]", "'"], args.fetch).split(', ')
-                    mol.data.update({ColumnNames['synonym']: row['synonym']})
-                    if 'inchi_key' in keys:
-                        keys = (', '.join(keys).replace("inchi_key", "inchi_key_first, inchi_key_last")).split(', ')
-                    [mol.data.update({ColumnNames[key]: row[key]}) for key in keys if key]
+                mol = pybel.readstring("sdf", row["mol"])
+                if args.oformat == "sdf":
+                    keys = filter(lambda x: x not in ["[", "]", "'"], args.fetch).split(
+                        ", "
+                    )
+                    mol.data.update({ColumnNames["synonym"]: row["synonym"]})
+                    if "inchi_key" in keys:
+                        keys = (
+                            ", ".join(keys).replace(
+                                "inchi_key", "inchi_key_first, inchi_key_last"
+                            )
+                        ).split(", ")
+                    [
+                        mol.data.update({ColumnNames[key]: row[key]})
+                        for key in keys
+                        if key
+                    ]
                 outfile.write(mol)
             except OSError:
                 pass
     else:
-        outfile = open(args.output, 'w')
-        outfile.write('\n'.join(['%s\t%s' % (row[args.oformat], row['synonym']) for row in rows]))
+        outfile = open(args.output, "w")
+        outfile.write(
+            "\n".join(["%s\t%s" % (row[args.oformat], row["synonym"]) for row in rows])
+        )
     outfile.close()
 
 
@@ -167,31 +200,37 @@
 
 def get_properties_ext(mol):
     HBD = pybel.Smarts("[!#6;!H0]")
-    HBA = pybel.Smarts(("[$([$([#8,#16]);!$(*=N~O);"
-                        "!$(*~N=O);X1,X2]),$([#7;v3;"
-                        "!$([nH]);!$(*(-a)-a)])]"
-                        ))
+    HBA = pybel.Smarts(
+        (
+            "[$([$([#8,#16]);!$(*=N~O);"
+            "!$(*~N=O);X1,X2]),$([#7;v3;"
+            "!$([nH]);!$(*(-a)-a)])]"
+        )
+    )
     calc_desc_dict = mol.calcdesc()
 
     try:
-        logp = calc_desc_dict['logP']
+        logp = calc_desc_dict["logP"]
     except KeyError:
-        logp = calc_desc_dict['LogP']
+        logp = calc_desc_dict["LogP"]
 
-    return {"molwt": mol.molwt,
-            "logp": logp,
-            "donors": len(HBD.findall(mol)),
-            "acceptors": len(HBA.findall(mol)),
-            "psa": calc_desc_dict['TPSA'],
-            "mr": calc_desc_dict['MR'],
-            "rotbonds": mol.OBMol.NumRotors(),
-            "can": mol.write("can").split()[0].strip(),  # tthis one works fine for both zinc and chembl (no ZINC code added after can descriptor string)
-            "inchi": mol.write("inchi").strip(),
-            "inchi_key": get_inchikey(mol).strip(),
-            "rings": len(mol.sssr),
-            "atoms": mol.OBMol.NumHvyAtoms(),
-            "spectrophore": OBspectrophore(mol),
-            }
+    return {
+        "molwt": mol.molwt,
+        "logp": logp,
+        "donors": len(HBD.findall(mol)),
+        "acceptors": len(HBA.findall(mol)),
+        "psa": calc_desc_dict["TPSA"],
+        "mr": calc_desc_dict["MR"],
+        "rotbonds": mol.OBMol.NumRotors(),
+        "can": mol.write("can")
+        .split()[0]
+        .strip(),  # tthis one works fine for both zinc and chembl (no ZINC code added after can descriptor string)
+        "inchi": mol.write("inchi").strip(),
+        "inchi_key": get_inchikey(mol).strip(),
+        "rings": len(mol.sssr),
+        "atoms": mol.OBMol.NumHvyAtoms(),
+        "spectrophore": OBspectrophore(mol),
+    }
 
 
 def get_inchikey(mol):
@@ -206,10 +245,12 @@
     spectrophore = pybel.ob.OBSpectrophore()
     # Parameters: rotation angle = 20, normalization for mean and sd, accuracy = 3.0 A and non-stereospecific cages.
     spectrophore.SetNormalization(spectrophore.NormalizationTowardsZeroMeanAndUnitStd)
-    return ', '.join(["%.3f" % value for value in spectrophore.GetSpectrophore(mol.OBMol)])
+    return ", ".join(
+        ["%.3f" % value for value in spectrophore.GetSpectrophore(mol.OBMol)]
+    )
 
 
-def split_library(lib_path, lib_format='sdf', package_size=None):
+def split_library(lib_path, lib_format="sdf", package_size=None):
     """
     Split a library of compounds. Usage: split_library(lib_path, lib_format, package_size)
     IT currently ONLY WORKS FOR SD-Files
@@ -217,18 +258,39 @@
     pack = 1
     mol_counter = 0
 
-    outfile = open('/%s/%s_pack_%i.%s' % ('/'.join(lib_path.split('/')[:-1]), lib_path.split('/')[-1].split('.')[0], pack, 'sdf'), 'w')
+    outfile = open(
+        "/%s/%s_pack_%i.%s"
+        % (
+            "/".join(lib_path.split("/")[:-1]),
+            lib_path.split("/")[-1].split(".")[0],
+            pack,
+            "sdf",
+        ),
+        "w",
+    )
 
-    for line in open(lib_path, 'r'):
+    for line in open(lib_path, "r"):
         outfile.write(line)
-        if line.strip() == '$$$$':
+        if line.strip() == "$$$$":
             mol_counter += 1
             if mol_counter % package_size == 0:
                 outfile.close()
                 pack += 1
-                outfile = open('/%s/%s_pack_%i.%s' % ('/'.join(lib_path.split('/')[:-1]), lib_path.split('/')[-1].split('.')[0], pack, 'sdf'), 'w')
+                outfile = open(
+                    "/%s/%s_pack_%i.%s"
+                    % (
+                        "/".join(lib_path.split("/")[:-1]),
+                        lib_path.split("/")[-1].split(".")[0],
+                        pack,
+                        "sdf",
+                    ),
+                    "w",
+                )
                 if mol_counter * 10 % package_size == 0:
-                    print('%i molecules parsed, starting pack nr. %i' % (mol_counter, pack - 1))
+                    print(
+                        "%i molecules parsed, starting pack nr. %i"
+                        % (mol_counter, pack - 1)
+                    )
     outfile.close()
 
     return True
@@ -242,7 +304,7 @@
     output_files = []
     tfile = tempfile.NamedTemporaryFile(delete=False)
 
-    smiles_handle = open(smiles_file, 'r')
+    smiles_handle = open(smiles_file, "r")
     for count, line in enumerate(smiles_handle):
         if count % structures_in_one_file == 0 and count != 0:
             tfile.close()
@@ -257,16 +319,19 @@
 
 def mp_run(input_path, regex, PROCESSES, function_to_call):
     paths = []
-    [paths.append(compound_file) for compound_file in glob.glob(str(input_path) + str(regex))]
+    [
+        paths.append(compound_file)
+        for compound_file in glob.glob(str(input_path) + str(regex))
+    ]
     paths.sort()
 
     pool = Pool(processes=PROCESSES)
-    print('Process initialized with', PROCESSES, 'processors')
+    print("Process initialized with", PROCESSES, "processors")
     result = pool.map_async(function_to_call, paths)
     result.get()
 
     return paths
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     print(check_filetype(sys.argv[1]))
--- a/distance_finder.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/distance_finder.py	Thu Aug 15 11:02:57 2024 +0000
@@ -19,9 +19,8 @@
 
 
 def log(*args, **kwargs):
-    """Log output to STDERR
-    """
-    print(*args, file=sys.stderr, ** kwargs)
+    """Log output to STDERR"""
+    print(*args, file=sys.stderr, **kwargs)
 
 
 def execute(ligands_sdf, points_file, outfile):
@@ -35,7 +34,7 @@
     points = []
 
     # read the points
-    with open(points_file, 'r') as f:
+    with open(points_file, "r") as f:
         for line in f.readlines():
             line.strip()
             if line:
@@ -45,7 +44,7 @@
                     log("Read points", p)
                     continue
             log("Failed to read line:", line)
-    log('Found', len(points), 'atom points')
+    log("Found", len(points), "atom points")
 
     sdf_writer = pybel.Outputfile("sdf", outfile, overwrite=True)
 
@@ -53,7 +52,7 @@
     for mol in pybel.readfile("sdf", ligands_sdf):
         count += 1
         if count % 50000 == 0:
-            log('Processed', count)
+            log("Processed", count)
 
         try:
             # print("Processing mol", mol.title)
@@ -70,32 +69,42 @@
                 distances = []
                 for i in coords:
                     # calculates distance based on cartesian coordinates
-                    distance = math.sqrt((point[0] - i[0])**2 + (point[1] - i[1])**2 + (point[2] - i[2])**2)
+                    distance = math.sqrt(
+                        (point[0] - i[0]) ** 2
+                        + (point[1] - i[1]) ** 2
+                        + (point[2] - i[2]) ** 2
+                    )
                     distances.append(distance)
                     # log("distance:", distance)
                 min_distance = min(distances)
                 # log('Min:', min_distance)
                 # log(count, p, min_distance)
 
-                mol.data['distance' + str(p)] = min_distance
+                mol.data["distance" + str(p)] = min_distance
 
             sdf_writer.write(mol)
 
         except Exception as e:
-            log('Failed to handle molecule: ' + str(e))
+            log("Failed to handle molecule: " + str(e))
             continue
 
     sdf_writer.close()
-    log('Wrote', count, 'molecules')
+    log("Wrote", count, "molecules")
 
 
 def main():
     global work_dir
 
-    parser = argparse.ArgumentParser(description='XChem distances - measure distances to particular points')
-    parser.add_argument('-i', '--input', help="SDF containing the 3D molecules to score)")
-    parser.add_argument('-p', '--points', help="PDB format file with atoms")
-    parser.add_argument('-o', '--outfile', default='output.sdf', help="File name for results")
+    parser = argparse.ArgumentParser(
+        description="XChem distances - measure distances to particular points"
+    )
+    parser.add_argument(
+        "-i", "--input", help="SDF containing the 3D molecules to score)"
+    )
+    parser.add_argument("-p", "--points", help="PDB format file with atoms")
+    parser.add_argument(
+        "-o", "--outfile", default="output.sdf", help="File name for results"
+    )
 
     args = parser.parse_args()
     log("XChem distances args: ", args)
--- a/multi_obgrep.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/multi_obgrep.py	Thu Aug 15 11:02:57 2024 +0000
@@ -15,21 +15,55 @@
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-i', '--infile', required=True, help='Molecule file.')
-    parser.add_argument('-q', '--query', required=True, help='Query file, containing different SMARTS in each line.')
-    parser.add_argument('-o', '--outfile', required=True, help='Path to the output file.')
+    parser.add_argument("-i", "--infile", required=True, help="Molecule file.")
+    parser.add_argument(
+        "-q",
+        "--query",
+        required=True,
+        help="Query file, containing different SMARTS in each line.",
+    )
+    parser.add_argument(
+        "-o", "--outfile", required=True, help="Path to the output file."
+    )
     parser.add_argument("--iformat", help="Input format, like smi, sdf, inchi")
-    parser.add_argument("--n-times", dest="n_times", type=int,
-                        default=0, help="Print a molecule only if the pattern occurs # times inside the molecule.")
-    parser.add_argument('-p', '--processors', type=int, default=multiprocessing.cpu_count())
-    parser.add_argument("--invert-matches", dest="invert_matches", action="store_true",
-                        default=False, help="Invert the matching, print non-matching molecules.")
-    parser.add_argument("--only-name", dest="only_name", action="store_true",
-                        default=False, help="Only print the name of the molecules.")
-    parser.add_argument("--full-match", dest="full_match", action="store_true",
-                        default=False, help="Full match, print matching-molecules only when the number of heavy atoms is also equal to the number of atoms in the SMARTS pattern.")
-    parser.add_argument("--number-of-matches", dest="number_of_matches", action="store_true",
-                        default=False, help="Print the number of matches.")
+    parser.add_argument(
+        "--n-times",
+        dest="n_times",
+        type=int,
+        default=0,
+        help="Print a molecule only if the pattern occurs # times inside the molecule.",
+    )
+    parser.add_argument(
+        "-p", "--processors", type=int, default=multiprocessing.cpu_count()
+    )
+    parser.add_argument(
+        "--invert-matches",
+        dest="invert_matches",
+        action="store_true",
+        default=False,
+        help="Invert the matching, print non-matching molecules.",
+    )
+    parser.add_argument(
+        "--only-name",
+        dest="only_name",
+        action="store_true",
+        default=False,
+        help="Only print the name of the molecules.",
+    )
+    parser.add_argument(
+        "--full-match",
+        dest="full_match",
+        action="store_true",
+        default=False,
+        help="Full match, print matching-molecules only when the number of heavy atoms is also equal to the number of atoms in the SMARTS pattern.",
+    )
+    parser.add_argument(
+        "--number-of-matches",
+        dest="number_of_matches",
+        action="store_true",
+        default=False,
+        help="Print the number of matches.",
+    )
     return parser.parse_args()
 
 
@@ -42,25 +76,27 @@
 
 def mp_helper(query, args):
     """
-        Helper function for multiprocessing.
-        That function is a wrapper around obgrep.
+    Helper function for multiprocessing.
+    That function is a wrapper around obgrep.
     """
 
     cmd_list = []
     if args.invert_matches:
-        cmd_list.append('-v')
+        cmd_list.append("-v")
     if args.only_name:
-        cmd_list.append('-n')
+        cmd_list.append("-n")
     if args.full_match:
-        cmd_list.append('-f')
+        cmd_list.append("-f")
     if args.number_of_matches:
-        cmd_list.append('-c')
+        cmd_list.append("-c")
     if args.n_times:
-        cmd_list.append('-t %s' % str(args.n_times))
+        cmd_list.append("-t %s" % str(args.n_times))
 
     tmp = tempfile.NamedTemporaryFile(delete=False)
-    cmd = 'obgrep %s "%s" %s' % (' '.join(cmd_list), query, args.infile)
-    child = subprocess.Popen(shlex.split(cmd), stdout=open(tmp.name, 'w+'), stderr=subprocess.PIPE)
+    cmd = 'obgrep %s "%s" %s' % (" ".join(cmd_list), query, args.infile)
+    child = subprocess.Popen(
+        shlex.split(cmd), stdout=open(tmp.name, "w+"), stderr=subprocess.PIPE
+    )
 
     stdout, stderr = child.communicate()
     return (tmp.name, query)
@@ -80,9 +116,9 @@
     pool.close()
     pool.join()
 
-    out_handle = open(args.outfile, 'wb')
+    out_handle = open(args.outfile, "wb")
     for result_file, query in results:
-        res_handle = open(result_file, 'rb')
+        res_handle = open(result_file, "rb")
         shutil.copyfileobj(res_handle, out_handle)
         res_handle.close()
         os.remove(result_file)
@@ -93,7 +129,7 @@
 
 def __main__():
     """
-        Multiprocessing obgrep search.
+    Multiprocessing obgrep search.
     """
     args = parse_command_line()
     obgrep(args)
--- a/ob_addh.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/ob_addh.py	Thu Aug 15 11:02:57 2024 +0000
@@ -7,16 +7,28 @@
 import sys
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line(argv):
     parser = argparse.ArgumentParser()
-    parser.add_argument('--iformat', type=str, default='sdf', help='input file format')
-    parser.add_argument('-i', '--input', type=str, required=True, help='input file name')
-    parser.add_argument('-o', '--output', type=str, required=True, help='output file name')
-    parser.add_argument('--polar', action="store_true", default=False, help='Add hydrogen atoms only to polar atoms')
-    parser.add_argument('--pH', type=float, default="7.4", help='Specify target pH value')
+    parser.add_argument("--iformat", type=str, default="sdf", help="input file format")
+    parser.add_argument(
+        "-i", "--input", type=str, required=True, help="input file name"
+    )
+    parser.add_argument(
+        "-o", "--output", type=str, required=True, help="output file name"
+    )
+    parser.add_argument(
+        "--polar",
+        action="store_true",
+        default=False,
+        help="Add hydrogen atoms only to polar atoms",
+    )
+    parser.add_argument(
+        "--pH", type=float, default="7.4", help="Specify target pH value"
+    )
     return parser.parse_args()
 
 
@@ -32,7 +44,7 @@
 
 def __main__():
     """
-        Add hydrogen atoms at a certain pH value
+    Add hydrogen atoms at a certain pH value
     """
     args = parse_command_line(sys.argv)
     addh(args)
--- a/ob_filter.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/ob_filter.py	Thu Aug 15 11:02:57 2024 +0000
@@ -14,33 +14,36 @@
 
 import cheminfolib
 from openbabel import pybel
+
 cheminfolib.pybel_stop_logging()
 
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-i', '--input', help='Input file name')
-    parser.add_argument('-iformat', help='Input file format')
-    parser.add_argument('-oformat', default='smi',
-                        help='Output file format')
-    parser.add_argument('-o', '--output', help='Output file name',
-                        required=True)
-    parser.add_argument('--filters', help="Specify the filters to apply",
-                        required=True)
-    parser.add_argument('--list_of_names', required=False,
-                        help="A file with list of molecule names to extract. Every name is in one line.")
+    parser.add_argument("-i", "--input", help="Input file name")
+    parser.add_argument("-iformat", help="Input file format")
+    parser.add_argument("-oformat", default="smi", help="Output file format")
+    parser.add_argument("-o", "--output", help="Output file name", required=True)
+    parser.add_argument("--filters", help="Specify the filters to apply", required=True)
+    parser.add_argument(
+        "--list_of_names",
+        required=False,
+        help="A file with list of molecule names to extract. Every name is in one line.",
+    )
     return parser.parse_args()
 
 
 def filter_precalculated_compounds(args, filters):
     outfile = pybel.Outputfile(args.oformat, args.output, overwrite=True)
-    for mol in pybel.readfile('sdf', args.input):
+    for mol in pybel.readfile("sdf", args.input):
         for key, elem in filters.items():
             # map the short description to the larger metadata names stored in the sdf file
             property = cheminfolib.ColumnNames.get(key, key)
             min = elem[0]
             max = elem[1]
-            if float(mol.data[property]) >= float(min) and float(mol.data[property]) <= float(max):
+            if float(mol.data[property]) >= float(min) and float(
+                mol.data[property]
+            ) <= float(max):
                 pass
             else:
                 # leave the filter loop, because one filter constrained are not satisfied
@@ -56,16 +59,30 @@
     if args.iformat == args.oformat:
         # use the -ocopy option from openbabel to speed up the filtering, additionally no conversion is carried out
         # http://openbabel.org/docs/dev/FileFormats/Copy_raw_text.html#copy-raw-text
-        cmd = 'obabel -i%s %s -ocopy -O %s --filter' % (args.iformat, args.input, args.output)
+        cmd = "obabel -i%s %s -ocopy -O %s --filter" % (
+            args.iformat,
+            args.input,
+            args.output,
+        )
     else:
-        cmd = 'obabel -i%s %s -o%s -O %s --filter' % (args.iformat, args.input, args.oformat, args.output)
-    filter_cmd = ''
+        cmd = "obabel -i%s %s -o%s -O %s --filter" % (
+            args.iformat,
+            args.input,
+            args.oformat,
+            args.output,
+        )
+    filter_cmd = ""
     # OBDescriptor stores a mapping from our desc shortcut to the OB name [0] and a long description [1]
     for key, elem in filters.items():
         ob_descriptor_name = cheminfolib.OBDescriptor[key][0]
         min = elem[0]
         max = elem[1]
-        filter_cmd += ' %s>=%s %s<=%s ' % (ob_descriptor_name, min, ob_descriptor_name, max)
+        filter_cmd += " %s>=%s %s<=%s " % (
+            ob_descriptor_name,
+            min,
+            ob_descriptor_name,
+            max,
+        )
 
     args = shlex.split('%s "%s"' % (cmd, filter_cmd))
     # print '%s "%s"' % (cmd, filter_cmd)
@@ -76,18 +93,18 @@
     return_code = child.returncode
 
     if return_code:
-        sys.stdout.write(stdout.decode('utf-8'))
-        sys.stderr.write(stderr.decode('utf-8'))
+        sys.stdout.write(stdout.decode("utf-8"))
+        sys.stderr.write(stderr.decode("utf-8"))
         sys.stderr.write("Return error code %i from command:\n" % return_code)
         sys.stderr.write("%s\n" % cmd)
     else:
-        sys.stdout.write(stdout.decode('utf-8'))
-        sys.stdout.write(stderr.decode('utf-8'))
+        sys.stdout.write(stdout.decode("utf-8"))
+        sys.stdout.write(stderr.decode("utf-8"))
 
 
 def filter_by_name(args):
     outfile = pybel.Outputfile(args.oformat, args.output, overwrite=True)
-    for mol in pybel.readfile('sdf', args.input):
+    for mol in pybel.readfile("sdf", args.input):
         for name in open(args.list_of_names):
             if mol.title.strip() == name.strip():
                 outfile.write(mol)
@@ -96,21 +113,21 @@
 
 def __main__():
     """
-        Select compounds with certain properties from a small library
+    Select compounds with certain properties from a small library
     """
     args = parse_command_line()
 
-    if args.filters == '__filter_by_name__':
+    if args.filters == "__filter_by_name__":
         filter_by_name(args)
         return
 
     # Its a small trick to get the parameters in an easy way from the xml file.
     # To keep it readable in the xml file, many white-spaces are included in that string it needs to be removed.
     # Also the last loop creates a ',{' that is not an valid jason expression.
-    filters = json.loads((args.filters).replace(' ', '').replace(',}', '}'))
-    if args.iformat == 'sdf':
+    filters = json.loads((args.filters).replace(" ", "").replace(",}", "}"))
+    if args.iformat == "sdf":
         # Check if the sdf file contains all of the required metadata to invoke the precalculation filtering
-        mol = next(pybel.readfile('sdf', args.input))
+        mol = next(pybel.readfile("sdf", args.input))
         for key, elem in filters.items():
             property = cheminfolib.ColumnNames.get(key, key)
             if property not in mol.data:
--- a/ob_genProp.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/ob_genProp.py	Thu Aug 15 11:02:57 2024 +0000
@@ -10,43 +10,57 @@
 import cheminfolib
 import openbabel
 from openbabel import pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line(argv):
     parser = argparse.ArgumentParser()
-    parser.add_argument('--iformat', default='sdf', help='input file format')
-    parser.add_argument('-i', '--input', required=True, help='input file name')
-    parser.add_argument('--oformat', default='sdf', choices=['sdf', 'table'], help='output file format')
-    parser.add_argument('--header', type=bool, help='Include the header as the first line of the output table')
-    parser.add_argument('-o', '--output', required=True, help='output file name')
+    parser.add_argument("--iformat", default="sdf", help="input file format")
+    parser.add_argument("-i", "--input", required=True, help="input file name")
+    parser.add_argument(
+        "--oformat", default="sdf", choices=["sdf", "table"], help="output file format"
+    )
+    parser.add_argument(
+        "--header",
+        type=bool,
+        help="Include the header as the first line of the output table",
+    )
+    parser.add_argument("-o", "--output", required=True, help="output file name")
     return parser.parse_args()
 
 
 def compute_properties(args):
-    if args.oformat == 'sdf':
+    if args.oformat == "sdf":
         outfile = pybel.Outputfile(args.oformat, args.output, overwrite=True)
     else:
-        outfile = open(args.output, 'w')
+        outfile = open(args.output, "w")
         if args.header:
             mol = next(pybel.readfile(args.iformat, args.input))
             metadata = cheminfolib.get_properties_ext(mol)
-            outfile.write('%s\n' % '\t'.join([cheminfolib.ColumnNames[key] for key in metadata]))
+            outfile.write(
+                "%s\n" % "\t".join([cheminfolib.ColumnNames[key] for key in metadata])
+            )
 
     for mol in pybel.readfile(args.iformat, args.input):
         if mol.OBMol.NumHvyAtoms() > 5:
             metadata = cheminfolib.get_properties_ext(mol)
-            if args.oformat == 'sdf':
-                [mol.data.update({cheminfolib.ColumnNames[key]: metadata[key]}) for key in metadata]
+            if args.oformat == "sdf":
+                [
+                    mol.data.update({cheminfolib.ColumnNames[key]: metadata[key]})
+                    for key in metadata
+                ]
                 outfile.write(mol)
             else:
-                outfile.write('%s\n' % ('\t'.join([str(metadata[key]) for key in metadata])))
+                outfile.write(
+                    "%s\n" % ("\t".join([str(metadata[key]) for key in metadata]))
+                )
     outfile.close()
 
 
 def __main__():
     """
-        Physico-chemical properties are computed and stored as metadata in the sdf output file
+    Physico-chemical properties are computed and stored as metadata in the sdf output file
     """
     args = parse_command_line(sys.argv)
     compute_properties(args)
--- a/ob_remIons.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/ob_remIons.py	Thu Aug 15 11:02:57 2024 +0000
@@ -8,37 +8,43 @@
 import argparse
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-iformat', default='sdf', help='input file format')
-    parser.add_argument('-i', '--input', required=True, help='input file name')
-    parser.add_argument('-o', '--output', required=True, help='output file name')
-    parser.add_argument('-idx', default=False, action='store_true', help='should output be an indexed text table? works only for inchi/smiles, otherwise is ignored')
+    parser.add_argument("-iformat", default="sdf", help="input file format")
+    parser.add_argument("-i", "--input", required=True, help="input file name")
+    parser.add_argument("-o", "--output", required=True, help="output file name")
+    parser.add_argument(
+        "-idx",
+        default=False,
+        action="store_true",
+        help="should output be an indexed text table? works only for inchi/smiles, otherwise is ignored",
+    )
     return parser.parse_args()
 
 
 def remove_ions(args):
-    with open(args.output, 'w') as outfile:
+    with open(args.output, "w") as outfile:
         for index, mol in enumerate(pybel.readfile(args.iformat, args.input)):
             if mol.OBMol.NumHvyAtoms() > 5:
                 mol.OBMol.StripSalts(0)
-                if 'inchi' in mol.data:
-                    del mol.data['inchi']  # remove inchi cache so modified mol is saved
+                if "inchi" in mol.data:
+                    del mol.data["inchi"]  # remove inchi cache so modified mol is saved
 
-            mol = mol.write(args.iformat) if mol.OBMol.NumHvyAtoms() > 5 else '\n'
+            mol = mol.write(args.iformat) if mol.OBMol.NumHvyAtoms() > 5 else "\n"
 
-            if args.idx and args.iformat in ['inchi', 'smi']:
-                outfile.write(f'{index}\t{mol}')
-            elif mol != '\n':
-                outfile.write(f'{mol}')
+            if args.idx and args.iformat in ["inchi", "smi"]:
+                outfile.write(f"{index}\t{mol}")
+            elif mol != "\n":
+                outfile.write(f"{mol}")
 
 
 def __main__():
     """
-        Remove any counterion and delete any fragment but the largest one for each molecule.
+    Remove any counterion and delete any fragment but the largest one for each molecule.
     """
     args = parse_command_line()
     remove_ions(args)
--- a/ob_spectrophore_search.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/ob_spectrophore_search.py	Thu Aug 15 11:02:57 2024 +0000
@@ -8,6 +8,7 @@
 
 import numpy as np
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 # TODO get rid of eval()
 
@@ -17,49 +18,94 @@
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('--target', required=True, help='target file name in sdf format with Spectrophores(TM) descriptors stored as meta-data')
-    parser.add_argument('--library', required=True, help='library of compounds with pre-computed physico-chemical properties, including Spectrophores(TM) in tabular format')
-    parser.add_argument('-c', '--column', required=True, type=int, help='#column containing the Spectrophores(TM) descriptors in the library file')
-    parser.add_argument('-o', '--output', required=True, help='output file name')
-    parser.add_argument('-n', '--normalization', default="ZeroMeanAndUnitStd", choices=['No', 'ZeroMean', 'UnitStd', 'ZeroMeanAndUnitStd'], help='Normalization method')
-    parser.add_argument('-a', '--accuracy', default="20", choices=['1', '2', '5', '10', '15', '20', '30', '36', '45', '60'], help='Accuracy expressed as angular stepsize')
-    parser.add_argument('-s', '--stereo', default="No", choices=['No', 'Unique', 'Mirror', 'All'], help='Stereospecificity of the cage')
-    parser.add_argument('-r', '--resolution', type=float, default="3.0", help='Resolution')
+    parser.add_argument(
+        "--target",
+        required=True,
+        help="target file name in sdf format with Spectrophores(TM) descriptors stored as meta-data",
+    )
+    parser.add_argument(
+        "--library",
+        required=True,
+        help="library of compounds with pre-computed physico-chemical properties, including Spectrophores(TM) in tabular format",
+    )
+    parser.add_argument(
+        "-c",
+        "--column",
+        required=True,
+        type=int,
+        help="#column containing the Spectrophores(TM) descriptors in the library file",
+    )
+    parser.add_argument("-o", "--output", required=True, help="output file name")
+    parser.add_argument(
+        "-n",
+        "--normalization",
+        default="ZeroMeanAndUnitStd",
+        choices=["No", "ZeroMean", "UnitStd", "ZeroMeanAndUnitStd"],
+        help="Normalization method",
+    )
+    parser.add_argument(
+        "-a",
+        "--accuracy",
+        default="20",
+        choices=["1", "2", "5", "10", "15", "20", "30", "36", "45", "60"],
+        help="Accuracy expressed as angular stepsize",
+    )
+    parser.add_argument(
+        "-s",
+        "--stereo",
+        default="No",
+        choices=["No", "Unique", "Mirror", "All"],
+        help="Stereospecificity of the cage",
+    )
+    parser.add_argument(
+        "-r", "--resolution", type=float, default="3.0", help="Resolution"
+    )
     return parser.parse_args()
 
 
 def set_parameters(args):
-    if args.normalization == 'No':
+    if args.normalization == "No":
         spectrophore.SetNormalization(spectrophore.NoNormalization)
     else:
-        spectrophore.SetNormalization(eval('spectrophore.NormalizationTowards' + args.normalization))
-    spectrophore.SetAccuracy(eval('spectrophore.AngStepSize' + args.accuracy))
-    spectrophore.SetStereo(eval('spectrophore.' + args.stereo + 'StereoSpecificProbes'))
+        spectrophore.SetNormalization(
+            eval("spectrophore.NormalizationTowards" + args.normalization)
+        )
+    spectrophore.SetAccuracy(eval("spectrophore.AngStepSize" + args.accuracy))
+    spectrophore.SetStereo(eval("spectrophore." + args.stereo + "StereoSpecificProbes"))
     spectrophore.SetResolution(args.resolution)
     return True
 
 
 def Compute_Spectrophores_distance(target_spectrophore, args):
-    outfile = open(args.output, 'w')
-    for mol in open(args.library, 'r'):
+    outfile = open(args.output, "w")
+    for mol in open(args.library, "r"):
         try:
-            distance = ((np.asarray(target_spectrophore, dtype=float) - np.asarray(mol.split('\t')[args.column - 1].strip().split(', '), dtype=float))**2).sum()
+            distance = (
+                (
+                    np.asarray(target_spectrophore, dtype=float)
+                    - np.asarray(
+                        mol.split("\t")[args.column - 1].strip().split(", "),
+                        dtype=float,
+                    )
+                )
+                ** 2
+            ).sum()
         except ValueError:
             distance = 0
-        outfile.write('%s\t%f\n' % (mol.strip(), distance))
+        outfile.write("%s\t%f\n" % (mol.strip(), distance))
     outfile.close()
 
 
 def __main__():
     """
-        Computation of Spectrophores(TM) distances to a target molecule.
+    Computation of Spectrophores(TM) distances to a target molecule.
     """
     args = parse_command_line()
     # This sets up the parameters for the Spectrophore generation. Parameters are set to fit those of our standard parsing tool
     set_parameters(args)
 
-    mol = next(pybel.readfile('sdf', args.target))
-    target_spectrophore = mol.data["Spectrophores(TM)"].strip().split(', ')
+    mol = next(pybel.readfile("sdf", args.target))
+    target_spectrophore = mol.data["Spectrophores(TM)"].strip().split(", ")
     # Compute the paired-distance between every molecule in the library and the target
     Compute_Spectrophores_distance(target_spectrophore, args)
 
--- a/remove_protonation_state.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/remove_protonation_state.py	Thu Aug 15 11:02:57 2024 +0000
@@ -7,14 +7,15 @@
 import argparse
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('--iformat', default='sdf', help='input file format')
-    parser.add_argument('-i', '--input', required=True, help='input file name')
-    parser.add_argument('-o', '--output', required=True, help='output file name')
+    parser.add_argument("--iformat", default="sdf", help="input file format")
+    parser.add_argument("-i", "--input", required=True, help="input file name")
+    parser.add_argument("-o", "--output", required=True, help="output file name")
     return parser.parse_args()
 
 
@@ -22,15 +23,15 @@
     outfile = pybel.Outputfile(args.iformat, args.output, overwrite=True)
     for mol in pybel.readfile(args.iformat, args.input):
         [atom.OBAtom.SetFormalCharge(0) for atom in mol.atoms]
-        if 'inchi' in mol.data:
-            del mol.data['inchi']  # remove inchi cache so modified mol is saved
+        if "inchi" in mol.data:
+            del mol.data["inchi"]  # remove inchi cache so modified mol is saved
         outfile.write(mol)
     outfile.close()
 
 
 def __main__():
     """
-        Remove any protonation state from each atom in each molecule.
+    Remove any protonation state from each atom in each molecule.
     """
     args = parse_command_line()
     remove_protonation(args)
--- a/subsearch.py	Tue Nov 10 20:30:04 2020 +0000
+++ b/subsearch.py	Thu Aug 15 11:02:57 2024 +0000
@@ -13,21 +13,34 @@
 import tempfile
 
 from openbabel import openbabel, pybel
+
 openbabel.obErrorLog.StopLogging()
 
 
 def parse_command_line():
     parser = argparse.ArgumentParser()
-    parser.add_argument('-i', '--infile', required=True, help='Molecule file.')
-    parser.add_argument('--iformat', help='Input format.')
-    parser.add_argument('--fastsearch-index', dest="fastsearch_index", required=True,
-                        help='Path to the openbabel fastsearch index.')
-    parser.add_argument('-o', '--outfile', required=True, help='Path to the output file.')
-    parser.add_argument('--oformat', default='smi', help='Output file format')
-    parser.add_argument("--max-candidates", dest="max_candidates", type=int, default=4000,
-                        help="The maximum number of candidates.")
-    parser.add_argument('-p', '--processors', type=int,
-                        default=multiprocessing.cpu_count())
+    parser.add_argument("-i", "--infile", required=True, help="Molecule file.")
+    parser.add_argument("--iformat", help="Input format.")
+    parser.add_argument(
+        "--fastsearch-index",
+        dest="fastsearch_index",
+        required=True,
+        help="Path to the openbabel fastsearch index.",
+    )
+    parser.add_argument(
+        "-o", "--outfile", required=True, help="Path to the output file."
+    )
+    parser.add_argument("--oformat", default="smi", help="Output file format")
+    parser.add_argument(
+        "--max-candidates",
+        dest="max_candidates",
+        type=int,
+        default=4000,
+        help="The maximum number of candidates.",
+    )
+    parser.add_argument(
+        "-p", "--processors", type=int, default=multiprocessing.cpu_count()
+    )
     return parser.parse_args()
 
 
@@ -40,20 +53,28 @@
 
 def mp_helper(query, args):
     """
-        Helper function for multiprocessing.
-        That function is a wrapper around the following command:
-        obabel file.fs -s"smarts" -Ooutfile.smi -al 999999999
+    Helper function for multiprocessing.
+    That function is a wrapper around the following command:
+    obabel file.fs -s"smarts" -Ooutfile.smi -al 999999999
     """
 
-    if args.oformat == 'names':
-        opts = '-osmi -xt'
+    if args.oformat == "names":
+        opts = "-osmi -xt"
     else:
-        opts = '-o%s' % args.oformat
+        opts = "-o%s" % args.oformat
 
     tmp = tempfile.NamedTemporaryFile(delete=False)
-    cmd = 'obabel -ifs %s -O %s %s -s%s -al %s' % (args.fastsearch_index, tmp.name, opts, query, args.max_candidates)
+    cmd = "obabel -ifs %s -O %s %s -s%s -al %s" % (
+        args.fastsearch_index,
+        tmp.name,
+        opts,
+        query,
+        args.max_candidates,
+    )
 
-    child = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    child = subprocess.Popen(
+        cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE
+    )
 
     stdout, stderr = child.communicate()
     return_code = child.returncode
@@ -73,14 +94,14 @@
     """
     Wrapper to retrieve a striped SMILES or SMARTS string from different input formats.
     """
-    if args.iformat in ['smi', 'text', 'tabular']:
+    if args.iformat in ["smi", "text", "tabular"]:
         with open(args.infile) as text_file:
             for line in text_file:
-                yield line.split('\t')[0].strip()
+                yield line.split("\t")[0].strip()
     else:
         # inchi or sdf files
         for mol in pybel.readfile(args.iformat, args.infile):
-            yield mol.write('smiles').split('\t')[0]
+            yield mol.write("smiles").split("\t")[0]
 
 
 def substructure_search(args):
@@ -91,18 +112,18 @@
     pool.close()
     pool.join()
 
-    if args.oformat == 'names':
-        out_handle = open(args.outfile, 'w')
+    if args.oformat == "names":
+        out_handle = open(args.outfile, "w")
         for result_file, query in results:
             with open(result_file) as res_handle:
                 for line in res_handle:
-                    out_handle.write('%s\t%s\n' % (line.strip(), query))
+                    out_handle.write("%s\t%s\n" % (line.strip(), query))
             os.remove(result_file)
         out_handle.close()
     else:
-        out_handle = open(args.outfile, 'wb')
+        out_handle = open(args.outfile, "wb")
         for result_file, query in results:
-            res_handle = open(result_file, 'rb')
+            res_handle = open(result_file, "rb")
             shutil.copyfileobj(res_handle, out_handle)
             res_handle.close()
             os.remove(result_file)
@@ -111,7 +132,7 @@
 
 def __main__():
     """
-        Multiprocessing Open Babel Substructure Search.
+    Multiprocessing Open Babel Substructure Search.
     """
     args = parse_command_line()
     substructure_search(args)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/2_mol.sdf	Thu Aug 15 11:02:57 2024 +0000
@@ -0,0 +1,66 @@
+CC(=O)Oc1ccccc1C(=O)[O-] InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)/p-1
+ OpenBabel08132415422D
+
+ 13 13  0  0  0  0  0  0  0  0999 V2000
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  5  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0  0  0  0
+  2  3  2  0  0  0  0
+  2  4  1  0  0  0  0
+  4  5  1  0  0  0  0
+  5 10  1  0  0  0  0
+  5  6  2  0  0  0  0
+  6  7  1  0  0  0  0
+  7  8  2  0  0  0  0
+  8  9  1  0  0  0  0
+  9 10  2  0  0  0  0
+ 10 11  1  0  0  0  0
+ 11 12  2  0  0  0  0
+ 11 13  1  0  0  0  0
+M  CHG  1  13  -1
+M  END
+$$$$
+CC(=O)Oc1ccccc1C(=O)[O-] InChI=1S/C9H8O4/c1-6(10)13-8-5-3-2-4-7(8)9(11)12/h2-5H,1H3,(H,11,12)/p-1
+ OpenBabel08132415422D
+
+ 13 13  0  0  0  0  0  0  0  0999 V2000
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
+    0.0000    0.0000    0.0000 O   0  5  0  0  0  0  0  0  0  0  0  0
+  1  2  1  0  0  0  0
+  2  3  2  0  0  0  0
+  2  4  1  0  0  0  0
+  4  5  1  0  0  0  0
+  5 10  1  0  0  0  0
+  5  6  2  0  0  0  0
+  6  7  1  0  0  0  0
+  7  8  2  0  0  0  0
+  8  9  1  0  0  0  0
+  9 10  2  0  0  0  0
+ 10 11  1  0  0  0  0
+ 11 12  2  0  0  0  0
+ 11 13  1  0  0  0  0
+M  CHG  1  13  -1
+M  END
+$$$$