Source code for nsaph_utils.utils.context

"""
Utilities to create context and configuration objects
"""
#  Copyright (c) 2021. Harvard University
#
#  Developed by Research Software Engineering,
#  Faculty of Arts and Sciences, Research Computing (FAS RC)
#  Author: Michael A Bouzinier
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
#

import argparse
import datetime
from enum import Enum
from typing import List


[docs]class Cardinality(Enum): """ Cardinality of a configuration parameter: multiple or singular """ single = "single" multiple = "multiple"
[docs]class Argument: """ A wrapper class to describe a command-line arguments This is practically, a more rigid format for :func ArgumentParser.add_argument: """ def __init__(self, name, help: str, type = str, aliases: List = None, default = None, cardinality: Cardinality = Cardinality.single, valid_values = None, required = True ): """ All arguments are passed to Argparser :param name: :param help: :param type: :param aliases: :param default: :param cardinality: :param valid_values: """ if aliases is None: aliases = [] self.name = name self.type = type self.aliases = aliases if self.type == bool and default is None: self.default = False else: self.default = default self.cardinality = cardinality self.description = help self.choices = valid_values self.required_flag = self.default is None and required return
[docs] def get_action(self): if self.type == bool: if not self.default: return 'store_true' return 'store_false' return None
[docs] def get_nargs(self): if self.cardinality == Cardinality.single: return None if self.default: return '*' return '+'
[docs] def get_help(self): if not self.is_required(): h = self.description if h.strip() and h.strip()[-1] not in {'.', ','}: h += ', ' h += "default: " + str(self.default) return h return self.description
[docs] def is_required(self): return self.required_flag
[docs] def add_to(self, parser): args = ["--" + self.name] for alias in self.aliases: if len(alias) == 1: args.append("-" + alias) else: args.append("--" + alias) action = self.get_action() nargs = self.get_nargs() kwargs = { "default": self.default, "help": self.get_help(), "required": self.is_required() } if action: kwargs['action'] = action else: kwargs["type"] = self.type if nargs: kwargs['nargs'] = nargs if self.choices: kwargs["choices"] = self.choices parser.add_argument(*args, **kwargs)
def __str__(self): return "--" + self.name
[docs]class Context: """ Generic class allowing to build context and configuration objects and initialize them using command line arguments """ _years = Argument("years", help=""" Year or list of years to download. For example, the following argument: `-y 1992:1995 1998 1999 2011 2015:2017` will produce the following list: [1992,1993,1994,1995,1998,1999,2011,2015,2016,2017] """, aliases=['y'], cardinality=Cardinality.multiple, default="1990:{}".format(datetime.date.today().year), ) _compress = Argument("compress", aliases=['c'], cardinality=Cardinality.single, type=bool, default=True, help="Use gzip compression for the result") def __init__(self, subclass, description = None, include_default: bool = True): """ Creates a new object :param subclass: A concrete class containing configuration information Configuration options must be defined as class memebers with names, starting with one '_' characters and values be instances of :class Argument: :param description: Optional text to use as description. If not specified, then it is extracted from subclass documentation """ self.arguments = None if include_default: self.years = None """ Year or list of years to download. For example, the following argument: `-y 1992:1995 1998 1999 2011 2015:2017` will produce the following list: [1992,1993,1994,1995,1998,1999,2011,2015,2016,2017] """ self.compress = None '''Specifies whether to use gzip compression for the result''' if description: self.description = description else: self.description = subclass.__doc__ self._attrs = [ field_name[1:] for field_name, field in subclass.__dict__.items() if isinstance(field, Argument) ] if include_default: self._attrs += [ field_name[1:] for field_name, field in Context.__dict__.items() if isinstance(field, Argument) and field_name[1:] not in self._attrs ]
[docs] def instantiate(self): self.arguments = [getattr(self, '_'+attr) for attr in self._attrs] return self._instantiate()
[docs] def set_empty_args(self): self.arguments = [ getattr(self, '_'+attr) for attr in self._attrs if getattr(self, attr) is None ] return self._instantiate()
def _instantiate(self): parser = argparse.ArgumentParser(self.description) for arg in self.arguments: arg.add_to(parser) args = parser.parse_args() for attr in self._attrs: current = getattr(self, attr, None) setattr(self, attr, self.validate(attr, getattr(args, attr, current)) ) return self
[docs] def default(self): for attr in self._attrs: arg: Argument = getattr(self, '_'+attr) setattr(self, attr, arg.default)
def __str__(self): return "; ".join([ "{}: {}".format(attr, getattr(self, attr)) for attr in self._attrs ])
[docs] def validate(self, attr, value): """ Subclasses can override this method to implement custom handling of command line arguments :param attr: Command line argument name :param value: Value returned by argparse :return: value to use """ if attr == "years": if type(value) is str: value = [value] years = [] if isinstance(value, str): value = [value] for y in value: if ':' in y: x = y.split(':') y1 = int(x[0]) y2 = int(x[1]) years += range(y1, y2 + 1) else: years.append(int(y)) return sorted(years) return value
[docs] @classmethod def enum(cls, enum_cls, s: str): """ A helper method to return Enum value by its name :param cls: Enum class :param s: name of a member in Enum class :return: value of the member """ d = {e.name: e for e in enum_cls} return d[s]