Source code for freephil.command_line

# Content in this file falls under the libtbx license

import os

import freephil

op = os.path


[docs] class argument_interpreter: """ Class of command line argument interpreter, based on base Phil object. The class is typically returned by :func:`freephil.scope.command_line_argument_interpreter` :param master_phil: base Phil object :type master_phil: freephil.scope :param home_scope: Home scope :type home_scope: str :param argument_description: Description of source of the arguments. Defaults to "command line" :type argument_description: str :param master_params: deprecated (raises warning about it) """ def __init__( self, master_phil=None, home_scope=None, argument_description=None, master_params=None, ): """ Class constructor """ if argument_description is None: argument_description = "command line " assert [master_params, master_phil].count(None) == 1 if master_phil is None: import warnings warnings.warn( message='The "master_params" keyword argument name is deprecated.' ' Please use "master_phil" instead.', category=DeprecationWarning, stacklevel=2, ) master_phil = master_params self.master_phil = master_phil self.home_scope = home_scope self.argument_description = argument_description self.target_paths = None def get_path_score(self, source_path, target_path): i = target_path.find(source_path) if i < 0: return 0 if i == 0 and len(source_path) == len(target_path): return 8 target_path_start_with_home_scope = False if self.home_scope is not None: if self.home_scope + "." + source_path == target_path: return 7 if target_path.startswith(self.home_scope + "."): if target_path.endswith("." + source_path): return 6 if target_path.endswith(source_path): return 5 target_path_start_with_home_scope = True if target_path_start_with_home_scope: return 2 if target_path.endswith("." + source_path): return 4 if target_path.endswith(source_path): return 3 return 1 def process_arg(self, arg): try: params = freephil.parse( input_string=arg, source_info=self.argument_description + "argument" ) except RuntimeError as e: raise freephil.Sorry( ( "Error interpreting %sargument as parameter definition:\n" f' "%s"\n {e.__class__.__name__}: {e!s}' ) % (self.argument_description, arg) ) if self.target_paths is None: self.target_paths = [ object_locator.path for object_locator in self.master_phil.all_definitions() ] def recursive_expert_level(phil_obj): if ( hasattr(phil_obj.object, "expert_level") and phil_obj.object.expert_level is not None ): return phil_obj.object.expert_level if not hasattr(phil_obj, "parent") or not phil_obj.parent: return 0 def parent_expert_level(obj): if hasattr(obj, "expert_level") and obj.expert_level is not None: return obj.expert_level if hasattr(obj, "primary_parent_scope") and obj.primary_parent_scope: return parent_expert_level(obj.primary_parent_scope) return 0 return parent_expert_level(phil_obj.parent) expert_level = [ recursive_expert_level(object_locator) for object_locator in self.master_phil.all_definitions() ] source_definitions = params.all_definitions() complete_definitions = "" for object_locator in source_definitions: object = object_locator.object scores = [ self.get_path_score(object_locator.path, target_path) for target_path in self.target_paths ] max_score = max(scores) if max_score == 0: raise freephil.Sorry( "Unknown %sparameter definition: %s" % (self.argument_description, object.as_str().strip()) ) if scores.count(max_score) > 1: error = ["Ambiguous parameter definition: %s" % object.as_str().strip()] error.append("Best matches:") for target_path, score in zip(self.target_paths, scores): if score == max_score: error.append(" " + target_path) # Calculate and apply tie-breaker value depending on expert level. # Arguments with lower expert level are preferentially # chosen if otherwise they would be ambiguous. scores = [ score - (exp_lvl / 100) for score, exp_lvl in zip(scores, expert_level) ] max_score = max(scores) if scores.count(max_score) > 1: raise freephil.Sorry("\n".join(error)) print( "Warning: " + "\n".join(error) + "\nAssuming %s was intended." % self.target_paths[scores.index(max_score)] ) complete_definitions += object.customized_copy( name=self.target_paths[scores.index(max_score)] ).as_str() if complete_definitions == "": raise freephil.Sorry( ( '%sparameter definition has no effect: "%s"' % (self.argument_description, arg) ).capitalize() ) return freephil.parse( input_string=complete_definitions, source_info=self.argument_description + "argument", ) def process_args(self, args, custom_processor=None): user_phils = [] for arg in args: if len(arg.strip()) == 0: continue if arg.startswith("--"): arg_work = arg[2:] if arg_work.find("=") < 0: arg_work += " = True" user_phils.append(self.process_arg(arg=arg_work)) continue if op.isfile(arg) and op.getsize(arg) > 0: try: user_phils.append(freephil.parse(file_name=arg)) except Exception: pass else: continue if arg.find("=") >= 0: try: user_phils.append(self.process_arg(arg=arg)) except (Exception, freephil.Sorry): pass else: continue if custom_processor is not None: result = custom_processor(arg=arg) if isinstance(result, freephil.scope): user_phils.append(result) continue elif (result is not None) and (result is not False): continue if op.isfile(arg): freephil.parse(file_name=arg) # exception expected raise RuntimeError( "Programming error or highly unusual situation" " (while processing %sargument %r)." % (self.argument_description, arg) ) raise freephil.Sorry( f"Uninterpretable {self.argument_description}argument: '{arg}'" ) return user_phils
[docs] def process(self, arg=None, args=None, custom_processor=None): """ Process string as command line argument. :param arg: One argument :type arg: str :param args: Arguments to be processed :type args: list of str :param custom_processor: Use custom Phil processor. :return: Phil object :rtype: freephil.scope """ assert [arg, args].count(None) == 1 if arg is not None: assert custom_processor is None return self.process_arg(arg=arg) return self.process_args(args=args, custom_processor=custom_processor)
[docs] def process_and_fetch(self, args, custom_processor=None, extra_sources=()): """ Performs process and fetch in single command. :param args: command line arguments :type args: list of strings :param custom_processor: If set to "collect_remaining", also unprocessed arguments are returned :param extra_sources: other sources to be fetched with the parsed arguments. :type extra_sources: list of freephil.scope :return: Phil object :rtype: freephil.scope """ if isinstance(custom_processor, str): assert custom_processor == "collect_remaining" remaining_args = [] def custom_processor(arg): remaining_args.append(arg) return True else: remaining_args = None sources = self.process(args=args, custom_processor=custom_processor) sources.extend(list(extra_sources)) result = self.master_phil.fetch(sources=sources) if remaining_args is None: return result return result, remaining_args
[docs] class process: """ Governing class for command line processing :param args: Input command line arguments :type args: list of str :param master_string: String defining base Phil :type master_string: str :param parse: Custom parser function, defaults to :class:`freephil.parse` :type parse: function :param extra_sources: Other Phil objects to be fetch with :type extra_sources: list of freephil.scope :ivar work: Phil object from processed arguments :vartype work: freephil.scope :ivar remaining_args: Arguments, which could not be parsed :vartype remaining_args: list of str :ivar master: Base Phil object :vartype master: freephil.scope """ def __init__(self, args, master_string, parse=None, extra_sources=()): """ Class constructor """ if parse is None: parse = freephil.parse self.parse = parse self.master = self.parse(input_string=master_string, process_includes=True) self.work, self.remaining_args = argument_interpreter( master_phil=self.master ).process_and_fetch( args=args, custom_processor="collect_remaining", extra_sources=extra_sources )
[docs] def show(self, out=None): """ Pretty prints the ``self.work``. :param out: Target of the print. If ``None``, prints to stdout :type out: None or file object """ self.work.show(out=out) return self