Spaces:
No application file
No application file
| # Copyright 2001-2004 Brad Chapman. | |
| # Revisions copyright 2009-2013 by Peter Cock. | |
| # All rights reserved. | |
| # | |
| # This file is part of the Biopython distribution and governed by your | |
| # choice of the "Biopython License Agreement" or the "BSD 3-Clause License". | |
| # Please see the LICENSE file that should have been included as part of this | |
| # package. | |
| """General mechanisms to access applications in Biopython (OBSOLETE). | |
| This module is not intended for direct use. It provides the basic objects which | |
| are subclassed by our command line wrappers, such as: | |
| - Bio.Align.Applications | |
| - Bio.Blast.Applications | |
| - Bio.Emboss.Applications | |
| - Bio.Sequencing.Applications | |
| These modules provide wrapper classes for command line tools to help you | |
| construct command line strings by setting the values of each parameter. | |
| The finished command line strings are then normally invoked via the built-in | |
| Python module subprocess. | |
| Due to the on going maintenance burden or keeping command line application | |
| wrappers up to date, we have decided to deprecate and eventually remove them. | |
| We instead now recommend building your command line and invoking it directly | |
| with the subprocess module. | |
| """ | |
| import os | |
| import platform | |
| import sys | |
| import subprocess | |
| import re | |
| # Use this regular expression to test the property names are going to | |
| # be valid as Python properties or arguments | |
| _re_prop_name = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$") | |
| assert _re_prop_name.match("t") | |
| assert _re_prop_name.match("test") | |
| assert _re_prop_name.match("_test") is None # we don't want private names | |
| assert _re_prop_name.match("-test") is None | |
| assert _re_prop_name.match("any-hyphen") is None | |
| assert _re_prop_name.match("underscore_ok") | |
| assert _re_prop_name.match("test_name") | |
| assert _re_prop_name.match("test2") | |
| # These are reserved names in Python itself, | |
| _reserved_names = [ | |
| "and", | |
| "del", | |
| "from", | |
| "not", | |
| "while", | |
| "as", | |
| "elif", | |
| "global", | |
| "or", | |
| "with", | |
| "assert", | |
| "else", | |
| "if", | |
| "pass", | |
| "yield", | |
| "break", | |
| "except", | |
| "import", | |
| "print", | |
| "class", | |
| "exec", | |
| "in", | |
| "raise", | |
| "continue", | |
| "finally", | |
| "is", | |
| "return", | |
| "def", | |
| "for", | |
| "lambda", | |
| "try", | |
| ] | |
| # These are reserved names due to the way the wrappers work | |
| _local_reserved_names = ["set_parameter"] | |
| class ApplicationError(subprocess.CalledProcessError): | |
| """Raised when an application returns a non-zero exit status (OBSOLETE). | |
| The exit status will be stored in the returncode attribute, similarly | |
| the command line string used in the cmd attribute, and (if captured) | |
| stdout and stderr as strings. | |
| This exception is a subclass of subprocess.CalledProcessError. | |
| >>> err = ApplicationError(-11, "helloworld", "", "Some error text") | |
| >>> err.returncode, err.cmd, err.stdout, err.stderr | |
| (-11, 'helloworld', '', 'Some error text') | |
| >>> print(err) | |
| Non-zero return code -11 from 'helloworld', message 'Some error text' | |
| """ | |
| def __init__(self, returncode, cmd, stdout="", stderr=""): | |
| """Initialize the class.""" | |
| self.returncode = returncode | |
| self.cmd = cmd | |
| self.stdout = stdout | |
| self.stderr = stderr | |
| def __str__(self): | |
| """Format the error as a string.""" | |
| # get first line of any stderr message | |
| try: | |
| msg = self.stderr.lstrip().split("\n", 1)[0].rstrip() | |
| except Exception: # TODO, ValueError? AttributeError? | |
| msg = "" | |
| if msg: | |
| return "Non-zero return code %d from %r, message %r" % ( | |
| self.returncode, | |
| self.cmd, | |
| msg, | |
| ) | |
| else: | |
| return "Non-zero return code %d from %r" % (self.returncode, self.cmd) | |
| def __repr__(self): | |
| """Represent the error as a string.""" | |
| return "ApplicationError(%i, %s, %s, %s)" % ( | |
| self.returncode, | |
| self.cmd, | |
| self.stdout, | |
| self.stderr, | |
| ) | |
| class AbstractCommandline: | |
| r"""Generic interface for constructing command line strings (OBSOLETE). | |
| This class shouldn't be called directly; it should be subclassed to | |
| provide an implementation for a specific application. | |
| For a usage example we'll show one of the EMBOSS wrappers. You can set | |
| options when creating the wrapper object using keyword arguments - or | |
| later using their corresponding properties: | |
| >>> from Bio.Emboss.Applications import WaterCommandline | |
| >>> cline = WaterCommandline(gapopen=10, gapextend=0.5) | |
| >>> cline | |
| WaterCommandline(cmd='water', gapopen=10, gapextend=0.5) | |
| You can instead manipulate the parameters via their properties, e.g. | |
| >>> cline.gapopen | |
| 10 | |
| >>> cline.gapopen = 20 | |
| >>> cline | |
| WaterCommandline(cmd='water', gapopen=20, gapextend=0.5) | |
| You can clear a parameter you have already added by 'deleting' the | |
| corresponding property: | |
| >>> del cline.gapopen | |
| >>> cline.gapopen | |
| >>> cline | |
| WaterCommandline(cmd='water', gapextend=0.5) | |
| Once you have set the parameters you need, you can turn the object into | |
| a string (e.g. to log the command): | |
| >>> str(cline) | |
| Traceback (most recent call last): | |
| ... | |
| ValueError: You must either set outfile (output filename), or enable filter or stdout (output to stdout). | |
| In this case the wrapper knows certain arguments are required to construct | |
| a valid command line for the tool. For a complete example, | |
| >>> from Bio.Emboss.Applications import WaterCommandline | |
| >>> water_cmd = WaterCommandline(gapopen=10, gapextend=0.5) | |
| >>> water_cmd.asequence = "asis:ACCCGGGCGCGGT" | |
| >>> water_cmd.bsequence = "asis:ACCCGAGCGCGGT" | |
| >>> water_cmd.outfile = "temp_water.txt" | |
| >>> print(water_cmd) | |
| water -outfile=temp_water.txt -asequence=asis:ACCCGGGCGCGGT -bsequence=asis:ACCCGAGCGCGGT -gapopen=10 -gapextend=0.5 | |
| >>> water_cmd | |
| WaterCommandline(cmd='water', outfile='temp_water.txt', asequence='asis:ACCCGGGCGCGGT', bsequence='asis:ACCCGAGCGCGGT', gapopen=10, gapextend=0.5) | |
| You would typically run the command line via a standard Python operating | |
| system call using the subprocess module for full control. For the simple | |
| case where you just want to run the command and get the output: | |
| stdout, stderr = water_cmd() | |
| Note that by default we assume the underlying tool is installed on the | |
| system $PATH environment variable. This is normal under Linux/Unix, but | |
| may need to be done manually under Windows. Alternatively, you can specify | |
| the full path to the binary as the first argument (cmd): | |
| >>> from Bio.Emboss.Applications import WaterCommandline | |
| >>> water_cmd = WaterCommandline(r"C:\Program Files\EMBOSS\water.exe", | |
| ... gapopen=10, gapextend=0.5, | |
| ... asequence="asis:ACCCGGGCGCGGT", | |
| ... bsequence="asis:ACCCGAGCGCGGT", | |
| ... outfile="temp_water.txt") | |
| >>> print(water_cmd) | |
| "C:\Program Files\EMBOSS\water.exe" -outfile=temp_water.txt -asequence=asis:ACCCGGGCGCGGT -bsequence=asis:ACCCGAGCGCGGT -gapopen=10 -gapextend=0.5 | |
| Notice that since the path name includes a space it has automatically | |
| been quoted. | |
| """ | |
| # TODO - Replace the above example since EMBOSS doesn't work properly | |
| # if installed into a folder with a space like "C:\Program Files\EMBOSS" | |
| # | |
| # Note the call example above is not a doctest as we can't handle EMBOSS | |
| # (or any other tool) being missing in the unit tests. | |
| parameters = None # will be a list defined in subclasses | |
| def __init__(self, cmd, **kwargs): | |
| """Create a new instance of a command line wrapper object.""" | |
| # Init method - should be subclassed! | |
| # | |
| # The subclass methods should look like this: | |
| # | |
| # def __init__(self, cmd="muscle", **kwargs): | |
| # self.parameters = [...] | |
| # AbstractCommandline.__init__(self, cmd, **kwargs) | |
| # | |
| # i.e. There should have an optional argument "cmd" to set the location | |
| # of the executable (with a sensible default which should work if the | |
| # command is on the path on Unix), and keyword arguments. It should | |
| # then define a list of parameters, all objects derived from the base | |
| # class _AbstractParameter. | |
| # | |
| # The keyword arguments should be any valid parameter name, and will | |
| # be used to set the associated parameter. | |
| self.program_name = cmd | |
| try: | |
| parameters = self.parameters | |
| except AttributeError: | |
| raise AttributeError( | |
| "Subclass should have defined self.parameters" | |
| ) from None | |
| # Create properties for each parameter at run time | |
| aliases = set() | |
| for p in parameters: | |
| if not p.names: | |
| if not isinstance(p, _StaticArgument): | |
| raise TypeError(f"Expected {p!r} to be of type _StaticArgument") | |
| continue | |
| for name in p.names: | |
| if name in aliases: | |
| raise ValueError(f"Parameter alias {name} multiply defined") | |
| aliases.add(name) | |
| name = p.names[-1] | |
| if _re_prop_name.match(name) is None: | |
| raise ValueError( | |
| "Final parameter name %r cannot be used as " | |
| "an argument or property name in python" % name | |
| ) | |
| if name in _reserved_names: | |
| raise ValueError( | |
| "Final parameter name %r cannot be used as " | |
| "an argument or property name because it is " | |
| "a reserved word in python" % name | |
| ) | |
| if name in _local_reserved_names: | |
| raise ValueError( | |
| "Final parameter name %r cannot be used as " | |
| "an argument or property name due to the " | |
| "way the AbstractCommandline class works" % name | |
| ) | |
| # Beware of binding-versus-assignment confusion issues | |
| def getter(name): | |
| return lambda x: x._get_parameter(name) | |
| def setter(name): | |
| return lambda x, value: x.set_parameter(name, value) | |
| def deleter(name): | |
| return lambda x: x._clear_parameter(name) | |
| doc = p.description | |
| if isinstance(p, _Switch): | |
| doc += ( | |
| "\n\nThis property controls the addition of the %s " | |
| "switch, treat this property as a boolean." % p.names[0] | |
| ) | |
| else: | |
| doc += ( | |
| "\n\nThis controls the addition of the %s parameter " | |
| "and its associated value. Set this property to the " | |
| "argument value required." % p.names[0] | |
| ) | |
| prop = property(getter(name), setter(name), deleter(name), doc) | |
| setattr(self.__class__, name, prop) # magic! | |
| for key, value in kwargs.items(): | |
| self.set_parameter(key, value) | |
| def _validate(self): | |
| """Make sure the required parameters have been set (PRIVATE). | |
| No return value - it either works or raises a ValueError. | |
| This is a separate method (called from __str__) so that subclasses may | |
| override it. | |
| """ | |
| for p in self.parameters: | |
| # Check for missing required parameters: | |
| if p.is_required and not (p.is_set): | |
| raise ValueError(f"Parameter {p.names[-1]} is not set.") | |
| # Also repeat the parameter validation here, just in case? | |
| def __str__(self): | |
| """Make the commandline string with the currently set options. | |
| e.g. | |
| >>> from Bio.Emboss.Applications import WaterCommandline | |
| >>> cline = WaterCommandline(gapopen=10, gapextend=0.5) | |
| >>> cline.asequence = "asis:ACCCGGGCGCGGT" | |
| >>> cline.bsequence = "asis:ACCCGAGCGCGGT" | |
| >>> cline.outfile = "temp_water.txt" | |
| >>> print(cline) | |
| water -outfile=temp_water.txt -asequence=asis:ACCCGGGCGCGGT -bsequence=asis:ACCCGAGCGCGGT -gapopen=10 -gapextend=0.5 | |
| >>> str(cline) | |
| 'water -outfile=temp_water.txt -asequence=asis:ACCCGGGCGCGGT -bsequence=asis:ACCCGAGCGCGGT -gapopen=10 -gapextend=0.5' | |
| """ | |
| self._validate() | |
| commandline = f"{_escape_filename(self.program_name)} " | |
| for parameter in self.parameters: | |
| if parameter.is_set: | |
| # This will include a trailing space: | |
| commandline += str(parameter) | |
| return commandline.strip() # remove trailing space | |
| def __repr__(self): | |
| """Return a representation of the command line object for debugging. | |
| e.g. | |
| >>> from Bio.Emboss.Applications import WaterCommandline | |
| >>> cline = WaterCommandline(gapopen=10, gapextend=0.5) | |
| >>> cline.asequence = "asis:ACCCGGGCGCGGT" | |
| >>> cline.bsequence = "asis:ACCCGAGCGCGGT" | |
| >>> cline.outfile = "temp_water.txt" | |
| >>> print(cline) | |
| water -outfile=temp_water.txt -asequence=asis:ACCCGGGCGCGGT -bsequence=asis:ACCCGAGCGCGGT -gapopen=10 -gapextend=0.5 | |
| >>> cline | |
| WaterCommandline(cmd='water', outfile='temp_water.txt', asequence='asis:ACCCGGGCGCGGT', bsequence='asis:ACCCGAGCGCGGT', gapopen=10, gapextend=0.5) | |
| """ | |
| answer = f"{self.__class__.__name__}(cmd={self.program_name!r}" | |
| for parameter in self.parameters: | |
| if parameter.is_set: | |
| if isinstance(parameter, _Switch): | |
| answer += f", {parameter.names[-1]}=True" | |
| else: | |
| answer += f", {parameter.names[-1]}={parameter.value!r}" | |
| answer += ")" | |
| return answer | |
| def _get_parameter(self, name): | |
| """Get a commandline option value (PRIVATE).""" | |
| for parameter in self.parameters: | |
| if name in parameter.names: | |
| if isinstance(parameter, _Switch): | |
| return parameter.is_set | |
| else: | |
| return parameter.value | |
| raise ValueError(f"Option name {name} was not found.") | |
| def _clear_parameter(self, name): | |
| """Reset or clear a commandline option value (PRIVATE).""" | |
| cleared_option = False | |
| for parameter in self.parameters: | |
| if name in parameter.names: | |
| parameter.value = None | |
| parameter.is_set = False | |
| cleared_option = True | |
| if not cleared_option: | |
| raise ValueError(f"Option name {name} was not found.") | |
| def set_parameter(self, name, value=None): | |
| """Set a commandline option for a program (OBSOLETE). | |
| Every parameter is available via a property and as a named | |
| keyword when creating the instance. Using either of these is | |
| preferred to this legacy set_parameter method which is now | |
| OBSOLETE, and likely to be DEPRECATED and later REMOVED in | |
| future releases. | |
| """ | |
| set_option = False | |
| for parameter in self.parameters: | |
| if name in parameter.names: | |
| if isinstance(parameter, _Switch): | |
| if value is None: | |
| import warnings | |
| warnings.warn( | |
| "For a switch type argument like %s, " | |
| "we expect a boolean. None is treated " | |
| "as FALSE!" % parameter.names[-1] | |
| ) | |
| parameter.is_set = bool(value) | |
| set_option = True | |
| else: | |
| if value is not None: | |
| self._check_value(value, name, parameter.checker_function) | |
| parameter.value = value | |
| parameter.is_set = True | |
| set_option = True | |
| if not set_option: | |
| raise ValueError(f"Option name {name} was not found.") | |
| def _check_value(self, value, name, check_function): | |
| """Check whether the given value is valid (PRIVATE). | |
| No return value - it either works or raises a ValueError. | |
| This uses the passed function 'check_function', which can either | |
| return a [0, 1] (bad, good) value or raise an error. Either way | |
| this function will raise an error if the value is not valid, or | |
| finish silently otherwise. | |
| """ | |
| if check_function is not None: | |
| is_good = check_function(value) # May raise an exception | |
| if is_good not in [0, 1, True, False]: | |
| raise ValueError( | |
| f"Result of check_function: {is_good!r} is of an unexpected value" | |
| ) | |
| if not is_good: | |
| raise ValueError( | |
| f"Invalid parameter value {value!r} for parameter {name}" | |
| ) | |
| def __setattr__(self, name, value): | |
| """Set attribute name to value (PRIVATE). | |
| This code implements a workaround for a user interface issue. | |
| Without this __setattr__ attribute-based assignment of parameters | |
| will silently accept invalid parameters, leading to known instances | |
| of the user assuming that parameters for the application are set, | |
| when they are not. | |
| >>> from Bio.Emboss.Applications import WaterCommandline | |
| >>> cline = WaterCommandline(gapopen=10, gapextend=0.5, stdout=True) | |
| >>> cline.asequence = "a.fasta" | |
| >>> cline.bsequence = "b.fasta" | |
| >>> cline.csequence = "c.fasta" | |
| Traceback (most recent call last): | |
| ... | |
| ValueError: Option name csequence was not found. | |
| >>> print(cline) | |
| water -stdout -asequence=a.fasta -bsequence=b.fasta -gapopen=10 -gapextend=0.5 | |
| This workaround uses a whitelist of object attributes, and sets the | |
| object attribute list as normal, for these. Other attributes are | |
| assumed to be parameters, and passed to the self.set_parameter method | |
| for validation and assignment. | |
| """ | |
| if name in ["parameters", "program_name"]: # Allowed attributes | |
| self.__dict__[name] = value | |
| else: | |
| self.set_parameter(name, value) # treat as a parameter | |
| def __call__(self, stdin=None, stdout=True, stderr=True, cwd=None, env=None): | |
| """Execute command, wait for it to finish, return (stdout, stderr). | |
| Runs the command line tool and waits for it to finish. If it returns | |
| a non-zero error level, an exception is raised. Otherwise two strings | |
| are returned containing stdout and stderr. | |
| The optional stdin argument should be a string of data which will be | |
| passed to the tool as standard input. | |
| The optional stdout and stderr argument may be filenames (string), | |
| but otherwise are treated as a booleans, and control if the output | |
| should be captured as strings (True, default), or ignored by sending | |
| it to /dev/null to avoid wasting memory (False). If sent to a file | |
| or ignored, then empty string(s) are returned. | |
| The optional cwd argument is a string giving the working directory | |
| to run the command from. See Python's subprocess module documentation | |
| for more details. | |
| The optional env argument is a dictionary setting the environment | |
| variables to be used in the new process. By default the current | |
| process' environment variables are used. See Python's subprocess | |
| module documentation for more details. | |
| Default example usage:: | |
| from Bio.Emboss.Applications import WaterCommandline | |
| water_cmd = WaterCommandline(gapopen=10, gapextend=0.5, | |
| stdout=True, auto=True, | |
| asequence="a.fasta", bsequence="b.fasta") | |
| print("About to run: %s" % water_cmd) | |
| std_output, err_output = water_cmd() | |
| This functionality is similar to subprocess.check_output(). In general | |
| if you require more control over running the command, use subprocess | |
| directly. | |
| When the program called returns a non-zero error level, a custom | |
| ApplicationError exception is raised. This includes any stdout and | |
| stderr strings captured as attributes of the exception object, since | |
| they may be useful for diagnosing what went wrong. | |
| """ | |
| if not stdout: | |
| stdout_arg = open(os.devnull, "w") | |
| elif isinstance(stdout, str): | |
| stdout_arg = open(stdout, "w") | |
| else: | |
| stdout_arg = subprocess.PIPE | |
| if not stderr: | |
| stderr_arg = open(os.devnull, "w") | |
| elif isinstance(stderr, str): | |
| if stdout == stderr: | |
| stderr_arg = stdout_arg # Write both to the same file | |
| else: | |
| stderr_arg = open(stderr, "w") | |
| else: | |
| stderr_arg = subprocess.PIPE | |
| # We may not need to supply any piped input, but we setup the | |
| # standard input pipe anyway as a work around for a python | |
| # bug if this is called from a Windows GUI program. For | |
| # details, see http://bugs.python.org/issue1124861 | |
| # | |
| # Using universal newlines is important on Python 3, this | |
| # gives unicode handles rather than bytes handles. | |
| # Windows 7, 8, 8.1 and 10 want shell = True | |
| if sys.platform != "win32": | |
| use_shell = True | |
| else: | |
| win_ver = platform.win32_ver()[0] | |
| if win_ver in ["7", "8", "post2012Server", "10"]: | |
| use_shell = True | |
| else: | |
| use_shell = False | |
| child_process = subprocess.Popen( | |
| str(self), | |
| stdin=subprocess.PIPE, | |
| stdout=stdout_arg, | |
| stderr=stderr_arg, | |
| universal_newlines=True, | |
| cwd=cwd, | |
| env=env, | |
| shell=use_shell, | |
| ) | |
| # Use .communicate as can get deadlocks with .wait(), see Bug 2804 | |
| stdout_str, stderr_str = child_process.communicate(stdin) | |
| if not stdout: | |
| assert not stdout_str, stdout_str | |
| if not stderr: | |
| assert not stderr_str, stderr_str | |
| return_code = child_process.returncode | |
| # Particularly important to close handles on Jython and PyPy | |
| # (where garbage collection is less predictable) and on Windows | |
| # (where cannot delete files with an open handle): | |
| if not stdout or isinstance(stdout, str): | |
| # We opened /dev/null or a file | |
| stdout_arg.close() | |
| if not stderr or (isinstance(stderr, str) and stdout != stderr): | |
| # We opened /dev/null or a file | |
| stderr_arg.close() | |
| if return_code: | |
| raise ApplicationError(return_code, str(self), stdout_str, stderr_str) | |
| return stdout_str, stderr_str | |
| class _AbstractParameter: | |
| """A class to hold information about a parameter for a commandline. | |
| Do not use this directly, instead use one of the subclasses. | |
| """ | |
| def __init__(self): | |
| raise NotImplementedError | |
| def __str__(self): | |
| raise NotImplementedError | |
| class _Option(_AbstractParameter): | |
| """Represent an option that can be set for a program. | |
| This holds UNIXish options like --append=yes and -a yes, | |
| where a value (here "yes") is generally expected. | |
| For UNIXish options like -kimura in clustalw which don't | |
| take a value, use the _Switch object instead. | |
| Attributes: | |
| - names -- a list of string names (typically two entries) by which | |
| the parameter can be set via the legacy set_parameter method | |
| (eg ["-a", "--append", "append"]). The first name in list is used | |
| when building the command line. The last name in the list is a | |
| "human readable" name describing the option in one word. This | |
| must be a valid Python identifier as it is used as the property | |
| name and as a keyword argument, and should therefore follow PEP8 | |
| naming. | |
| - description -- a description of the option. This is used as | |
| the property docstring. | |
| - filename -- True if this argument is a filename (or other argument | |
| that should be quoted) and should be automatically quoted if it | |
| contains spaces. | |
| - checker_function -- a reference to a function that will determine | |
| if a given value is valid for this parameter. This function can either | |
| raise an error when given a bad value, or return a [0, 1] decision on | |
| whether the value is correct. | |
| - equate -- should an equals sign be inserted if a value is used? | |
| - is_required -- a flag to indicate if the parameter must be set for | |
| the program to be run. | |
| - is_set -- if the parameter has been set | |
| - value -- the value of a parameter | |
| """ | |
| def __init__( | |
| self, | |
| names, | |
| description, | |
| filename=False, | |
| checker_function=None, | |
| is_required=False, | |
| equate=True, | |
| ): | |
| self.names = names | |
| if not isinstance(description, str): | |
| raise TypeError(f"Should be a string: {description!r} for {names[-1]}") | |
| # Note 'filename' is for any string with spaces that needs quoting | |
| self.is_filename = filename | |
| self.checker_function = checker_function | |
| self.description = description | |
| self.equate = equate | |
| self.is_required = is_required | |
| self.is_set = False | |
| self.value = None | |
| def __str__(self): | |
| """Return the value of this option for the commandline. | |
| Includes a trailing space. | |
| """ | |
| # Note: Before equate was handled explicitly, the old | |
| # code would do either "--name " or "--name=value ", | |
| # or " -name " or " -name value ". This choice is now | |
| # now made explicitly when setting up the option. | |
| if self.value is None: | |
| return f"{self.names[0]} " | |
| if self.is_filename: | |
| v = _escape_filename(self.value) | |
| else: | |
| v = str(self.value) | |
| if self.equate: | |
| return f"{self.names[0]}={v} " | |
| else: | |
| return f"{self.names[0]} {v} " | |
| class _Switch(_AbstractParameter): | |
| """Represent an optional argument switch for a program. | |
| This holds UNIXish options like -kimura in clustalw which don't | |
| take a value, they are either included in the command string | |
| or omitted. | |
| Attributes: | |
| - names -- a list of string names (typically two entries) by which | |
| the parameter can be set via the legacy set_parameter method | |
| (eg ["-a", "--append", "append"]). The first name in list is used | |
| when building the command line. The last name in the list is a | |
| "human readable" name describing the option in one word. This | |
| must be a valid Python identifier as it is used as the property | |
| name and as a keyword argument, and should therefore follow PEP8 | |
| naming. | |
| - description -- a description of the option. This is used as | |
| the property docstring. | |
| - is_set -- if the parameter has been set | |
| NOTE - There is no value attribute, see is_set instead, | |
| """ | |
| def __init__(self, names, description): | |
| self.names = names | |
| self.description = description | |
| self.is_set = False | |
| self.is_required = False | |
| def __str__(self): | |
| """Return the value of this option for the commandline. | |
| Includes a trailing space. | |
| """ | |
| assert not hasattr(self, "value") | |
| if self.is_set: | |
| return f"{self.names[0]} " | |
| else: | |
| return "" | |
| class _Argument(_AbstractParameter): | |
| """Represent an argument on a commandline. | |
| The names argument should be a list containing one string. | |
| This must be a valid Python identifier as it is used as the | |
| property name and as a keyword argument, and should therefore | |
| follow PEP8 naming. | |
| """ | |
| def __init__( | |
| self, | |
| names, | |
| description, | |
| filename=False, | |
| checker_function=None, | |
| is_required=False, | |
| ): | |
| # if len(names) != 1: | |
| # raise ValueError("The names argument to _Argument should be a " | |
| # "single entry list with a PEP8 property name.") | |
| self.names = names | |
| if not isinstance(description, str): | |
| raise TypeError(f"Should be a string: {description!r} for {names[-1]}") | |
| # Note 'filename' is for any string with spaces that needs quoting | |
| self.is_filename = filename | |
| self.checker_function = checker_function | |
| self.description = description | |
| self.is_required = is_required | |
| self.is_set = False | |
| self.value = None | |
| def __str__(self): | |
| if self.value is None: | |
| return " " | |
| elif self.is_filename: | |
| return f"{_escape_filename(self.value)} " | |
| else: | |
| return f"{self.value} " | |
| class _ArgumentList(_Argument): | |
| """Represent a variable list of arguments on a command line, e.g. multiple filenames.""" | |
| # TODO - Option to require at least one value? e.g. min/max count? | |
| def __str__(self): | |
| if not isinstance(self.value, list): | |
| raise TypeError("Arguments should be a list") | |
| if not self.value: | |
| raise ValueError("Requires at least one filename") | |
| # A trailing space is required so that parameters following the last filename | |
| # do not appear merged. | |
| # e.g.: samtools cat in1.bam in2.bam-o out.sam [without trailing space][Incorrect] | |
| # samtools cat in1.bam in2.bam -o out.sam [with trailing space][Correct] | |
| if self.is_filename: | |
| return " ".join(_escape_filename(v) for v in self.value) + " " | |
| else: | |
| return " ".join(self.value) + " " | |
| class _StaticArgument(_AbstractParameter): | |
| """Represent a static (read only) argument on a commandline. | |
| This is not intended to be exposed as a named argument or | |
| property of a command line wrapper object. | |
| """ | |
| def __init__(self, value): | |
| self.names = [] | |
| self.is_required = False | |
| self.is_set = True | |
| self.value = value | |
| def __str__(self): | |
| return f"{self.value} " | |
| def _escape_filename(filename): | |
| """Escape filenames with spaces by adding quotes (PRIVATE). | |
| Note this will not add quotes if they are already included: | |
| >>> print((_escape_filename('example with spaces'))) | |
| "example with spaces" | |
| >>> print((_escape_filename('"example with spaces"'))) | |
| "example with spaces" | |
| >>> print((_escape_filename(1))) | |
| 1 | |
| Note the function is more generic than the name suggests, since it | |
| is used to add quotes around any string arguments containing spaces. | |
| """ | |
| # Is adding the following helpful | |
| # if os.path.isfile(filename): | |
| # # On Windows, if the file exists, we can ask for | |
| # # its alternative short name (DOS style 8.3 format) | |
| # # which has no spaces in it. Note that this name | |
| # # is not portable between machines, or even folder! | |
| # try: | |
| # import win32api | |
| # short = win32api.GetShortPathName(filename) | |
| # assert os.path.isfile(short) | |
| # return short | |
| # except ImportError: | |
| # pass | |
| if not isinstance(filename, str): | |
| # for example the NCBI BLAST+ -outfmt argument can be an integer | |
| return filename | |
| if " " not in filename: | |
| return filename | |
| # We'll just quote it - works on Windows, Mac OS X etc | |
| if filename.startswith('"') and filename.endswith('"'): | |
| # Its already quoted | |
| return filename | |
| else: | |
| return f'"{filename}"' | |
| def _test(): | |
| """Run the Bio.Application module's doctests (PRIVATE).""" | |
| import doctest | |
| doctest.testmod(verbose=1) | |
| if __name__ == "__main__": | |
| # Run the doctests | |
| _test() | |