Source code for robot.model.modelobject

#  Copyright 2008-2015 Nokia Networks
#  Copyright 2016-     Robot Framework Foundation
#
#  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 copy
import json
from pathlib import Path
from typing import Any, Dict, overload, TextIO, Type, TypeVar

from robot.errors import DataError
from robot.utils import get_error_message, SetterAwareType, type_name


T = TypeVar('T', bound='ModelObject')
DataDict = Dict[str, Any]


[docs] class ModelObject(metaclass=SetterAwareType): repr_args = () __slots__ = []
[docs] @classmethod def from_dict(cls: Type[T], data: DataDict) -> T: """Create this object based on data in a dictionary. Data can be got from the :meth:`to_dict` method or created externally. With ``robot.running`` model objects new in Robot Framework 6.1, with ``robot.result`` new in Robot Framework 7.0. """ try: return cls().config(**data) except (AttributeError, TypeError) as err: raise DataError(f"Creating '{full_name(cls)}' object from dictionary " f"failed: {err}")
[docs] @classmethod def from_json(cls: Type[T], source: 'str|bytes|TextIO|Path') -> T: """Create this object based on JSON data. The data is given as the ``source`` parameter. It can be: - a string (or bytes) containing the data directly, - an open file object where to read the data from, or - a path (``pathlib.Path`` or string) to a UTF-8 encoded file to read. The JSON data is first converted to a Python dictionary and the object created using the :meth:`from_dict` method. Notice that the ``source`` is considered to be JSON data if it is a string and contains ``{``. If you need to use ``{`` in a file system path, pass it in as a ``pathlib.Path`` instance. With ``robot.running`` model objects new in Robot Framework 6.1, with ``robot.result`` new in Robot Framework 7.0. """ try: data = JsonLoader().load(source) except (TypeError, ValueError) as err: raise DataError(f'Loading JSON data failed: {err}') return cls.from_dict(data)
[docs] def to_dict(self) -> DataDict: """Serialize this object into a dictionary. The object can be later restored by using the :meth:`from_dict` method. With ``robot.running`` model objects new in Robot Framework 6.1, with ``robot.result`` new in Robot Framework 7.0. """ raise NotImplementedError
@overload def to_json(self, file: None = None, *, ensure_ascii: bool = False, indent: int = 0, separators: 'tuple[str, str]' = (',', ':')) -> str: ... @overload def to_json(self, file: 'TextIO|Path|str', *, ensure_ascii: bool = False, indent: int = 0, separators: 'tuple[str, str]' = (',', ':')) -> None: ...
[docs] def to_json(self, file: 'None|TextIO|Path|str' = None, *, ensure_ascii: bool = False, indent: int = 0, separators: 'tuple[str, str]' = (',', ':')) -> 'str|None': """Serialize this object into JSON. The object is first converted to a Python dictionary using the :meth:`to_dict` method and then the dictionary is converted to JSON. The ``file`` parameter controls what to do with the resulting JSON data. It can be: - ``None`` (default) to return the data as a string, - an open file object where to write the data to, or - a path (``pathlib.Path`` or string) to a file where to write the data using UTF-8 encoding. JSON formatting can be configured using optional parameters that are passed directly to the underlying json__ module. Notice that the defaults differ from what ``json`` uses. With ``robot.running`` model objects new in Robot Framework 6.1, with ``robot.result`` new in Robot Framework 7.0. __ https://docs.python.org/3/library/json.html """ return JsonDumper(ensure_ascii=ensure_ascii, indent=indent, separators=separators).dump(self.to_dict(), file)
[docs] def config(self: T, **attributes) -> T: """Configure model object with given attributes. ``obj.config(name='Example', doc='Something')`` is equivalent to setting ``obj.name = 'Example'`` and ``obj.doc = 'Something'``. New in Robot Framework 4.0. """ for name, value in attributes.items(): try: orig = getattr(self, name) except AttributeError: raise AttributeError(f"'{full_name(self)}' object does not have " f"attribute '{name}'") # Preserve tuples. Main motivation is converting lists with `from_json`. if isinstance(orig, tuple) and not isinstance(value, tuple): try: value = tuple(value) except TypeError: raise TypeError(f"'{full_name(self)}' object attribute '{name}' " f"is 'tuple', got '{type_name(value)}'.") try: setattr(self, name, value) except AttributeError as err: # Ignore error setting attribute if the object already has it. # Avoids problems with `from_dict` with body items having # un-settable `type` attribute that is needed in dict data. if value != orig: raise AttributeError(f"Setting attribute '{name}' failed: {err}") return self
[docs] def copy(self: T, **attributes) -> T: """Return a shallow copy of this object. :param attributes: Attributes to be set to the returned copy. For example, ``obj.copy(name='New name')``. See also :meth:`deepcopy`. The difference between ``copy`` and ``deepcopy`` is the same as with the methods having same names in the copy__ module. __ https://docs.python.org/3/library/copy.html """ return copy.copy(self).config(**attributes)
[docs] def deepcopy(self: T, **attributes) -> T: """Return a deep copy of this object. :param attributes: Attributes to be set to the returned copy. For example, ``obj.deepcopy(name='New name')``. See also :meth:`copy`. The difference between ``deepcopy`` and ``copy`` is the same as with the methods having same names in the copy__ module. __ https://docs.python.org/3/library/copy.html """ return copy.deepcopy(self).config(**attributes)
def __repr__(self) -> str: args = [] for name in self.repr_args: value = getattr(self, name) if self._include_in_repr(name, value): value = self._repr_format(name, value) args.append(f'{name}={value}') return f"{full_name(self)}({', '.join(args)})" def _include_in_repr(self, name: str, value: Any) -> bool: return True def _repr_format(self, name: str, value: Any) -> str: return repr(value)
[docs] def full_name(obj_or_cls): cls = type(obj_or_cls) if not isinstance(obj_or_cls, type) else obj_or_cls parts = cls.__module__.split('.') + [cls.__name__] if len(parts) > 1 and parts[0] == 'robot': parts[2:-1] = [] return '.'.join(parts)
[docs] class JsonLoader:
[docs] def load(self, source: 'str|bytes|TextIO|Path') -> DataDict: try: data = self._load(source) except (json.JSONDecodeError, TypeError): raise ValueError(f'Invalid JSON data: {get_error_message()}') if not isinstance(data, dict): raise TypeError(f"Expected dictionary, got {type_name(data)}.") return data
def _load(self, source): if self._is_path(source): with open(source, encoding='UTF-8') as file: return json.load(file) if hasattr(source, 'read'): return json.load(source) return json.loads(source) def _is_path(self, source): if isinstance(source, Path): return True if not isinstance(source, str) or '{' in source: return False try: return Path(source).is_file() except OSError: # Can happen on Windows w/ Python < 3.10. return False
[docs] class JsonDumper: def __init__(self, **config): self.config = config @overload def dump(self, data: DataDict, output: None = None) -> str: ... @overload def dump(self, data: DataDict, output: 'TextIO|Path|str') -> None: ...
[docs] def dump(self, data: DataDict, output: 'None|TextIO|Path|str' = None) -> 'None|str': if not output: return json.dumps(data, **self.config) elif isinstance(output, (str, Path)): with open(output, 'w', encoding='UTF-8') as file: json.dump(data, file, **self.config) elif hasattr(output, 'write'): json.dump(data, output, **self.config) else: raise TypeError(f"Output should be None, path or open file, " f"got {type_name(output)}.")