# 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