diff --git a/ChangeLog b/ChangeLog index ef73418a..5778e5c1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,14 @@ CHANGES ======= +* Switch to a better behaved jsonasobj + +v0.1.1 +------ + +* Automated adding outputs from tests +* v0.1.0 tag + v0.1.0 ------ diff --git a/Pipfile b/Pipfile index 173c88d5..e3afa8e0 100644 --- a/Pipfile +++ b/Pipfile @@ -5,8 +5,10 @@ name = "pypi" [packages] hbreader = "*" +# Note: pyld needs requests package to access contexts +requests = "*" pyld = { git = "https://github.com/hsolbrig/pyld" } -jsonasobj = "==2.0.2dev1" +jsonasobj = ">=2.0.2dev4" pyyaml = ">=5.1" rdflib = "*" rdflib-pyld-compat = "*" @@ -16,7 +18,6 @@ prefixcommons = "*" shexjsg = "*" [dev-packages] -requests = "*" [pipenv] allow_prereleases = true diff --git a/linkml_runtime/utils/dataclass_extensions_376.py b/linkml_runtime/utils/dataclass_extensions_376.py index 6e13aa00..62969750 100644 --- a/linkml_runtime/utils/dataclass_extensions_376.py +++ b/linkml_runtime/utils/dataclass_extensions_376.py @@ -1,51 +1,107 @@ -from dataclasses import MISSING, _HAS_DEFAULT_FACTORY, _POST_INIT_NAME, _FIELD_INITVAR, _init_param, _field_init, _create_fn - - -def dataclasses_init_fn_with_kwargs(fields, frozen, has_post_init, self_name, globals): - # fields contains both real fields and InitVar pseudo-fields. - - # Make sure we don't have fields without defaults following fields - # with defaults. This actually would be caught when exec-ing the - # function source code, but catching it here gives a better error - # message, and future-proofs us in case we build up the function - # using ast. - seen_default = False - for f in fields: - # Only consider fields in the __init__ call. - if f.init: - if not (f.default is MISSING and f.default_factory is MISSING): - seen_default = True - elif seen_default: - raise TypeError(f'non-default argument {f.name!r} ' - 'follows default argument') - - locals = {f'_type_{f.name}': f.type for f in fields} - locals.update({ - 'MISSING': MISSING, - '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY, - }) - - body_lines = [] - for f in fields: - line = _field_init(f, frozen, locals, self_name) - # line is None means that this field doesn't require - # initialization (it's a pseudo-field). Just skip it. - if line: - body_lines.append(line) - - # Does this class have a post-init function? - if has_post_init: - params_str = ','.join(f.name for f in fields - if f._field_type is _FIELD_INITVAR) - body_lines.append(f'{self_name}.{_POST_INIT_NAME}({params_str}{", " if params_str else ""} **kwargs)') - - # If no body lines, use 'pass'. - if not body_lines: - body_lines = ['pass'] - - return _create_fn('__init__', - [self_name] + [_init_param(f) for f in fields if f.init] + ["**kwargs"], - body_lines, - locals=locals, - globals=globals, - return_type=None) \ No newline at end of file +import sys + +if sys.version_info < (3, 7, 0): + raise NotImplementedError("LinkML requires Python 3.7 or later to run") +elif sys.version_info >= (3, 7, 6): + from dataclasses import MISSING, _HAS_DEFAULT_FACTORY, _POST_INIT_NAME, _FIELD_INITVAR, _init_param, _field_init, _create_fn + + + def dataclasses_init_fn_with_kwargs(fields, frozen, has_post_init, self_name, globals): + # fields contains both real fields and InitVar pseudo-fields. + + # Make sure we don't have fields without defaults following fields + # with defaults. This actually would be caught when exec-ing the + # function source code, but catching it here gives a better error + # message, and future-proofs us in case we build up the function + # using ast. + seen_default = False + for f in fields: + # Only consider fields in the __init__ call. + if f.init: + if not (f.default is MISSING and f.default_factory is MISSING): + seen_default = True + elif seen_default: + raise TypeError(f'non-default argument {f.name!r} ' + 'follows default argument') + + locals = {f'_type_{f.name}': f.type for f in fields} + locals.update({ + 'MISSING': MISSING, + '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY, + }) + + body_lines = [] + for f in fields: + line = _field_init(f, frozen, locals, self_name) + # line is None means that this field doesn't require + # initialization (it's a pseudo-field). Just skip it. + if line: + body_lines.append(line) + + # Does this class have a post-init function? + if has_post_init: + params_str = ','.join(f.name for f in fields + if f._field_type is _FIELD_INITVAR) + body_lines.append(f'{self_name}.{_POST_INIT_NAME}({params_str}{", " if params_str else ""} **kwargs)') + + # If no body lines, use 'pass'. + if not body_lines: + body_lines = ['pass'] + + return _create_fn('__init__', + [self_name] + [_init_param(f) for f in fields if f.init] + ["**kwargs"], + body_lines, + locals=locals, + globals=globals, + return_type=None) +else: + from dataclasses import MISSING, _HAS_DEFAULT_FACTORY, _POST_INIT_NAME, _FIELD_INITVAR, _init_param, _field_init, \ + _create_fn + + + def dataclasses_init_fn_with_kwargs(fields, frozen, has_post_init, self_name): + # fields contains both real fields and InitVar pseudo-fields. + + # Make sure we don't have fields without defaults following fields + # with defaults. This actually would be caught when exec-ing the + # function source code, but catching it here gives a better error + # message, and future-proofs us in case we build up the function + # using ast. + seen_default = False + for f in fields: + # Only consider fields in the __init__ call. + if f.init: + if not (f.default is MISSING and f.default_factory is MISSING): + seen_default = True + elif seen_default: + raise TypeError(f'non-default argument {f.name!r} ' + 'follows default argument') + + globals = {'MISSING': MISSING, + '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY} + + body_lines = [] + for f in fields: + line = _field_init(f, frozen, globals, self_name) + # line is None means that this field doesn't require + # initialization (it's a pseudo-field). Just skip it. + if line: + body_lines.append(line) + + # Does this class have a post-init function? + if has_post_init: + params_str = ','.join(f.name for f in fields + if f._field_type is _FIELD_INITVAR) + body_lines.append(f'{self_name}.{_POST_INIT_NAME}({params_str}{", " if params_str else ""} **kwargs)') + + # If no body lines, use 'pass'. + if not body_lines: + body_lines = ['pass'] + + locals = {f'_type_{f.name}': f.type for f in fields} + return _create_fn('__init__', + [self_name] + [_init_param(f) for f in fields if f.init] + ["**kwargs"], + body_lines, + locals=locals, + globals=globals, + return_type=None) diff --git a/linkml_runtime/utils/yamlutils.py b/linkml_runtime/utils/yamlutils.py index d239a868..2ce6231b 100644 --- a/linkml_runtime/utils/yamlutils.py +++ b/linkml_runtime/utils/yamlutils.py @@ -1,13 +1,15 @@ from copy import copy -from typing import Union, Any, List, Optional, Type, Callable +from json import JSONDecoder +from typing import Union, Any, List, Optional, Type, Callable, Dict import yaml -from jsonasobj import JsonObj, as_json, ExtendedNamespace +from jsonasobj import JsonObj, as_json, ExtendedNamespace, as_dict, JsonObjTypes, JsonTypes, items from rdflib import Graph from yaml.constructor import ConstructorError from linkml_runtime.utils.context_utils import CONTEXTS_PARAM_TYPE, merge_contexts +YAMLObjTypes = Union[JsonObjTypes, "YAMLRoot"] class YAMLMark(yaml.error.Mark): def __str__(self): @@ -43,7 +45,7 @@ def _default(self, obj, filtr: Callable[[dict], dict] = None): :return: Serialized version of obj """ - if isinstance(obj, JsonObj): + if isinstance(obj, YAMLRoot): rval = dict() for k, v in (filtr(obj.__dict__) if filtr else obj.__dict__).items(): is_classvar = k.startswith("type_") and hasattr(type(obj), k) @@ -71,7 +73,7 @@ def _default(self, obj, filtr: Callable[[dict], dict] = None): rval[k] = v return rval else: - return super()._default(obj) + return obj._default(obj) if hasattr(obj, '_default') and callable(obj._default) else JSONDecoder().decode(obj) def _normalize_inlined_slot(self, slot_name: str, slot_type: Type, key_name: Optional[str], inlined_as_list: Optional[bool], keyed: bool) -> None: @@ -87,6 +89,8 @@ def _normalize_inlined_slot(self, slot_name: str, slot_type: Type, key_name: Opt @param keyed: True means each identifier must be unique """ raw_slot: Union[list, dict] = self[slot_name] + if isinstance(raw_slot, JsonObj): + raw_slot = as_dict(raw_slot) cooked_slot = list() if inlined_as_list else dict() key_list = list() @@ -146,6 +150,24 @@ def cook_a_slot(entry) -> None: raise ValueError(f"Slot: {slot_name} must be a dictionary or a list") self[slot_name] = cooked_slot + @staticmethod + def _as_list(value: List[JsonObjTypes]) -> List[JsonTypes]: + """ Return a json array as a list + + :param value: array + :return: array with JsonObj instances removed + """ + return [e._as_dict if isinstance(e, YAMLRoot) else e for e in value] + + @property + def _as_dict(self) -> Dict[str, JsonTypes]: + """ Convert a JsonObj into a straight dictionary + + :return: dictionary that cooresponds to the json object + """ + return {k: v._as_dict if isinstance(v, YAMLRoot) else self._as_list(v) if isinstance(v, list) else v + for k, v in items(self)} + def root_representer(dumper: yaml.Dumper, data: YAMLRoot): """ YAML callback -- used to filter out empty values (None, {}, [] and false) diff --git a/tests/support/mismatchlog.py b/tests/support/mismatchlog.py index 57a6b8ba..0523c161 100644 --- a/tests/support/mismatchlog.py +++ b/tests/support/mismatchlog.py @@ -15,11 +15,11 @@ class MismatchLog: difference_text - the details on the difference. Used for RDF and other non-ascii files """ class MismatchLogEntry: - @dataclass class StackFrame: - filename: str - method: str - line: int + def __init__(self, filename: str, method: str, line: int) -> None: + self.filename = filename + self.method = method + self.line = line def __str__(self): return f'File "{self.filename}", line {self.line} in {self.method} ' diff --git a/tox.ini b/tox.ini index 3a64d70b..76eef09c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py37, py38, py39 setenv = PIPENV_SKIP_LOCK=1 PIPENV_DEV=1 PIPENV_IGNORE_VIRTUALENVS=1 + PIPENV_VERBOSITY=-1 [testenv] whitelist_externals = python