def recognize(self, node: yaml.Node, expected_type: Type) -> RecResult: """Figure out how to interpret this node. This is not quite a type check. This function makes a list of all types that match the expected type and also the node, and returns that list. The goal here is not to test validity, but to determine how to process this node further. That said, it will recognize built-in types only in case of an exact match. Args: node: The YAML node to recognize. expected_type: The type we expect this node to be, based on the context provided by our type definitions. Returns: A list of matching types. """ logger.debug('Recognizing {} as a {}'.format(node, expected_type)) recognized_types = None # type: Any if expected_type in (str, int, float, bool, bool_union_fix, date, None, type(None)): recognized_types, message = self.__recognize_scalar( node, expected_type) elif expected_type in self.__additional_classes: recognized_types, message = self.__recognize_additional( node, expected_type) elif is_generic_union(expected_type): recognized_types, message = self.__recognize_union( node, expected_type) elif is_generic_sequence(expected_type): recognized_types, message = self.__recognize_list( node, expected_type) elif is_generic_mapping(expected_type): recognized_types, message = self.__recognize_dict( node, expected_type) elif expected_type in self.__registered_classes.values(): recognized_types, message = self.__recognize_user_classes( node, expected_type) elif expected_type in (Any, ): recognized_types, message = [Any], '' if recognized_types is None: raise RecognitionError( ('Could not recognize for type {},' ' is it registered?').format(expected_type.__name__)) logger.debug('Recognized types {} matching {}'.format( recognized_types, expected_type)) return recognized_types, message
def __type_matches(self, obj: Any, type_: Type) -> bool: """Checks that the object matches the given type. Like isinstance(), but will work with union types using Union, Dict and List. Args: obj: The object to check type_: The type to check against Returns: True iff obj is of type type_ """ if is_generic_union(type_): for t in generic_type_args(type_): if self.__type_matches(obj, t): return True return False elif is_generic_sequence(type_): if not isinstance(obj, list): return False for item in obj: if not self.__type_matches(item, generic_type_args(type_)[0]): return False return True elif is_generic_mapping(type_): if not isinstance(obj, OrderedDict): return False for key, value in obj.items(): if not isinstance(key, generic_type_args(type_)[0]): return False if not self.__type_matches(value, generic_type_args(type_)[1]): return False return True elif type_ is bool_union_fix: return isinstance(obj, bool) elif type_ is Any: return True else: return isinstance(obj, type_)
def load_function(result=_AnyYAML, *args): # type: ignore """Create a load function for the given type. This function returns a callable object which takes an input (``str`` with YAML input, ``pathlib.Path``, or an open stream) and tries to load an object of the type given as the first argument. Any user-defined classes needed by the result must be passed as the remaining arguments. Note that mypy will give an error if you try to pass some of the special type-like objects from ``typing``. ``typing.Dict`` and ``typing.List`` seem to be okay, but ``typing.Union``, ``typing.Optional``, and abstract containers ``typing.Sequence``, ``typing.Mapping``, ``typing.MutableSequence`` and ``typing.MutableMapping`` will give an error. They are supported however, and work fine, there is just no way presently to explain to mypy that they are okay. So, if you want to tell YAtiML that your YAML file may contain either a string or an int, you can use ``Union[str, int]`` for the first argument, but you'll have to add a ``# type: ignore`` or two to tell mypy to ignore the issue. The resulting Callable will have return type ``Any`` in this case. Examples: .. code-block:: python load_int_dict = yatiml.load_function(Dict[str, int]) my_dict = load_int_dict('x: 1') .. code-block:: python load_config = yatiml.load_function(Config, Setting) my_config = load_config(Path('config.yaml')) # or with open('config.yaml', 'r') as f: my_config = load_config(f) Here, ``Config`` is the top-level class, and ``Setting`` is another class that is used by ``Config`` somewhere. .. code-block:: python # Needs an ignore, on each line if split over two lines load_int_or_str = yatiml.load_function( # type: ignore Union[int, str]) # type: ignore Args: result: The top level type, return type of the function. *args: Any other (custom) types needed. Returns: A function that can load YAML input from a string, Path or stream and convert it to an object of the first type given. """ class UserLoader(Loader): pass # add loaders for additional types if UserLoader._additional_classes is None: UserLoader._additional_classes = dict() UserLoader.add_constructor('!Path', PathConstructor()) UserLoader._additional_classes[Path] = '!Path' additional_types = (Path, ) # add loaders for user types user_classes = list(args) if not (is_generic_mapping(result) or is_generic_sequence(result) or is_generic_union(result) or result is Any): if result not in additional_types and result not in user_classes: user_classes.append(result) add_to_loader(UserLoader, user_classes) if result is _AnyYAML: set_document_type(UserLoader, Any) # type: ignore else: set_document_type(UserLoader, result) class LoadFunction: """Validates YAML input and constructs objects.""" def __init__(self, loader: Type[Loader]) -> None: """Create a LoadFunction.""" self.loader = loader def __call__(self, source: Union[str, Path, IO[AnyStr]]) -> T: """Load a YAML document from a source. The source can be a string containing YAML, a pathlib.Path containing a path to a file to load, or a stream (e.g. an open file handle returned by open()). Args: source: The source to load from. Returns: An object loaded from the file. Raises: yatiml.RecognitionError: If the input is invalid. """ if isinstance(source, Path): with source.open('r') as f: return cast(T, yaml.load(f, Loader=self.loader)) else: return cast(T, yaml.load(source, Loader=self.loader)) return LoadFunction(UserLoader)