mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-01 23:08:43 +01:00
* build(deps-dev): bump mypy from 1.6.1 to 1.7.0 Bumps [mypy](https://github.com/python/mypy) from 1.6.1 to 1.7.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.6.1...v1.7.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> * fix misc mypy errors --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ruslan Kuprieiev <[email protected]>
337 lines
10 KiB
Python
337 lines
10 KiB
Python
import os
|
|
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
|
|
|
from funcy import first, last
|
|
|
|
from dvc.exceptions import DvcException
|
|
from dvc.render import FILENAME_FIELD, INDEX_FIELD, VERSION_FIELD
|
|
|
|
from . import Converter
|
|
|
|
|
|
class FieldNotFoundError(DvcException):
|
|
def __init__(self, expected_field, found_fields):
|
|
found_str = ", ".join(found_fields)
|
|
super().__init__(
|
|
f"Could not find provided field ('{expected_field}') "
|
|
f"in data fields ('{found_str}')."
|
|
)
|
|
|
|
|
|
def _lists(blob: Union[Dict, List]) -> Iterable[List]:
|
|
if isinstance(blob, list):
|
|
yield blob
|
|
else:
|
|
for _, value in blob.items():
|
|
if isinstance(value, dict):
|
|
yield from _lists(value)
|
|
elif isinstance(value, list):
|
|
yield value
|
|
|
|
|
|
def _file_field(*args):
|
|
for axis_def in args:
|
|
if axis_def is not None:
|
|
for file, val in axis_def.items():
|
|
if isinstance(val, str):
|
|
yield file, val
|
|
elif isinstance(val, list):
|
|
for field in val:
|
|
yield file, field
|
|
|
|
|
|
def _find(
|
|
filename: str,
|
|
field: str,
|
|
data_series: List[Tuple[str, str, Any]],
|
|
):
|
|
for data_file, data_field, data in data_series:
|
|
if data_file == filename and data_field == field:
|
|
return data_file, data_field, data
|
|
return None
|
|
|
|
|
|
def _verify_field(file2datapoints: Dict[str, List], filename: str, field: str):
|
|
if filename in file2datapoints:
|
|
datapoint = first(file2datapoints[filename])
|
|
if field not in datapoint:
|
|
raise FieldNotFoundError(field, datapoint.keys())
|
|
|
|
|
|
def _get_xs(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
|
|
x = properties.get("x", None)
|
|
if x is not None and isinstance(x, dict):
|
|
for filename, field in _file_field(x):
|
|
_verify_field(file2datapoints, filename, field)
|
|
yield filename, field
|
|
|
|
|
|
def _get_ys(properties, file2datapoints: Dict[str, List[Dict]]):
|
|
y = properties.get("y", None)
|
|
if y is not None:
|
|
for filename, field in _file_field(y):
|
|
_verify_field(file2datapoints, filename, field)
|
|
yield filename, field
|
|
|
|
|
|
def _is_datapoints(lst: List[Dict]):
|
|
"""
|
|
check if dict keys match, datapoints with different keys mgiht lead
|
|
to unexpected behavior
|
|
"""
|
|
|
|
return all(isinstance(item, dict) for item in lst) and set(first(lst).keys()) == {
|
|
key for keys in lst for key in keys
|
|
}
|
|
|
|
|
|
def get_datapoints(file_content: Dict):
|
|
result: List[Dict[str, Any]] = []
|
|
for lst in _lists(file_content):
|
|
if _is_datapoints(lst):
|
|
for index, datapoint in enumerate(lst):
|
|
if len(result) <= index:
|
|
result.append({})
|
|
result[index].update(datapoint)
|
|
return result
|
|
|
|
|
|
class VegaConverter(Converter):
|
|
"""
|
|
Class that takes care of converting unspecified data blob
|
|
(Dict or List[Dict]) into datapoints (List[Dict]).
|
|
If some properties that are required by Template class are missing
|
|
('x', 'y') it will attempt to fill in the blanks.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
plot_id: str,
|
|
data: Optional[Dict] = None,
|
|
properties: Optional[Dict] = None,
|
|
):
|
|
super().__init__(plot_id, data, properties)
|
|
self.plot_id = plot_id
|
|
self.inferred_properties: Dict = {}
|
|
|
|
def _infer_y_from_data(self):
|
|
if self.plot_id in self.data:
|
|
for lst in _lists(self.data[self.plot_id]):
|
|
if all(isinstance(item, dict) for item in lst):
|
|
datapoint = first(lst)
|
|
field = last(datapoint.keys())
|
|
self.inferred_properties["y"] = {self.plot_id: field}
|
|
break
|
|
|
|
def _infer_x_y(self):
|
|
x = self.properties.get("x", None)
|
|
y = self.properties.get("y", None)
|
|
|
|
# Infer x.
|
|
if isinstance(x, str):
|
|
self.inferred_properties["x"] = {}
|
|
# If multiple y files, duplicate x for each file.
|
|
if isinstance(y, dict):
|
|
for file, fields in y.items():
|
|
# Duplicate x for each y.
|
|
if isinstance(fields, list):
|
|
self.inferred_properties["x"][file] = [x] * len(fields)
|
|
else:
|
|
self.inferred_properties["x"][file] = x
|
|
# Otherwise use plot ID as file.
|
|
else:
|
|
self.inferred_properties["x"][self.plot_id] = x
|
|
|
|
# Infer y.
|
|
if y is None:
|
|
self._infer_y_from_data()
|
|
# If y files not provided, use plot ID as file.
|
|
elif not isinstance(y, dict):
|
|
self.inferred_properties["y"] = {self.plot_id: y}
|
|
|
|
def _find_datapoints(self):
|
|
result = {}
|
|
for file, content in self.data.items():
|
|
result[file] = get_datapoints(content)
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
def infer_y_label(properties):
|
|
y_label = properties.get("y_label", None)
|
|
if y_label is not None:
|
|
return y_label
|
|
y = properties.get("y", None)
|
|
if isinstance(y, str):
|
|
return y
|
|
if isinstance(y, list):
|
|
return "y"
|
|
if not isinstance(y, dict):
|
|
return
|
|
|
|
fields = {field for _, field in _file_field(y)}
|
|
if len(fields) == 1:
|
|
return first(fields)
|
|
return "y"
|
|
|
|
@staticmethod
|
|
def infer_x_label(properties):
|
|
x_label = properties.get("x_label", None)
|
|
if x_label is not None:
|
|
return x_label
|
|
|
|
x = properties.get("x", None)
|
|
if not isinstance(x, dict):
|
|
return INDEX_FIELD
|
|
|
|
fields = {field for _, field in _file_field(x)}
|
|
if len(fields) == 1:
|
|
return first(fields)
|
|
return "x"
|
|
|
|
def flat_datapoints(self, revision): # noqa: C901, PLR0912
|
|
file2datapoints, properties = self.convert()
|
|
|
|
props_update = {}
|
|
|
|
xs = list(_get_xs(properties, file2datapoints))
|
|
|
|
# assign "step" if no x provided
|
|
if not xs:
|
|
x_file, x_field = (
|
|
None,
|
|
INDEX_FIELD,
|
|
)
|
|
else:
|
|
x_file, x_field = xs[0]
|
|
props_update["x"] = x_field
|
|
|
|
ys = list(_get_ys(properties, file2datapoints))
|
|
|
|
num_xs = len(xs)
|
|
num_ys = len(ys)
|
|
if num_xs > 1 and num_xs != num_ys:
|
|
raise DvcException(
|
|
"Cannot have different number of x and y data sources. Found "
|
|
f"{num_xs} x and {num_ys} y data sources."
|
|
)
|
|
|
|
all_datapoints = []
|
|
if ys:
|
|
_all_y_files, _all_y_fields = list(zip(*ys))
|
|
all_y_fields = set(_all_y_fields)
|
|
all_y_files = set(_all_y_files)
|
|
else:
|
|
all_y_files = set()
|
|
all_y_fields = set()
|
|
|
|
# override to unified y field name if there are different y fields
|
|
if len(all_y_fields) > 1:
|
|
props_update["y"] = "dvc_inferred_y_value"
|
|
else:
|
|
props_update["y"] = first(all_y_fields)
|
|
|
|
# get common prefix to drop from file names
|
|
if len(all_y_files) > 1:
|
|
common_prefix_len = len(os.path.commonpath(list(all_y_files)))
|
|
else:
|
|
common_prefix_len = 0
|
|
|
|
for i, (y_file, y_field) in enumerate(ys):
|
|
if num_xs > 1:
|
|
x_file, x_field = xs[i]
|
|
datapoints = [{**d} for d in file2datapoints.get(y_file, [])]
|
|
|
|
if props_update.get("y", None) == "dvc_inferred_y_value":
|
|
_update_from_field(
|
|
datapoints,
|
|
field="dvc_inferred_y_value",
|
|
source_field=y_field,
|
|
)
|
|
|
|
if x_field == INDEX_FIELD and x_file is None:
|
|
_update_from_index(datapoints, INDEX_FIELD)
|
|
else:
|
|
x_datapoints = file2datapoints.get(x_file, [])
|
|
try:
|
|
_update_from_field(
|
|
datapoints,
|
|
field=x_field,
|
|
source_datapoints=x_datapoints,
|
|
)
|
|
except IndexError:
|
|
raise DvcException( # noqa: B904
|
|
f"Cannot join '{x_field}' from '{x_file}' and "
|
|
f"'{y_field}' from '{y_file}'. "
|
|
"They have to have same length."
|
|
)
|
|
|
|
y_file_short = y_file[common_prefix_len:].strip("/\\")
|
|
_update_all(
|
|
datapoints,
|
|
update_dict={
|
|
VERSION_FIELD: {
|
|
"revision": revision,
|
|
FILENAME_FIELD: y_file_short,
|
|
"field": y_field,
|
|
}
|
|
},
|
|
)
|
|
|
|
all_datapoints.extend(datapoints)
|
|
|
|
if not all_datapoints:
|
|
return [], {}
|
|
|
|
properties = {**properties, **props_update}
|
|
|
|
return all_datapoints, properties
|
|
|
|
def convert(
|
|
self,
|
|
):
|
|
"""
|
|
Convert the data. Fill necessary fields ('x', 'y') and return both
|
|
generated datapoints and updated properties. `x`, `y` values and labels
|
|
are inferred and always provided.
|
|
"""
|
|
self._infer_x_y()
|
|
|
|
datapoints = self._find_datapoints()
|
|
properties = {**self.properties, **self.inferred_properties}
|
|
|
|
properties["y_label"] = self.infer_y_label(properties)
|
|
properties["x_label"] = self.infer_x_label(properties)
|
|
|
|
return datapoints, properties
|
|
|
|
|
|
def _update_from_field(
|
|
target_datapoints: List[Dict],
|
|
field: str,
|
|
source_datapoints: Optional[List[Dict]] = None,
|
|
source_field: Optional[str] = None,
|
|
):
|
|
if source_datapoints is None:
|
|
source_datapoints = target_datapoints
|
|
if source_field is None:
|
|
source_field = field
|
|
|
|
if len(source_datapoints) != len(target_datapoints):
|
|
raise IndexError("Source and target datapoints must have the same length")
|
|
|
|
for index, datapoint in enumerate(target_datapoints):
|
|
source_datapoint = source_datapoints[index]
|
|
if source_field in source_datapoint:
|
|
datapoint[field] = source_datapoint[source_field]
|
|
|
|
|
|
def _update_from_index(datapoints: List[Dict], new_field: str):
|
|
for index, datapoint in enumerate(datapoints):
|
|
datapoint[new_field] = index
|
|
|
|
|
|
def _update_all(datapoints: List[Dict], update_dict: Dict):
|
|
for datapoint in datapoints:
|
|
datapoint.update(update_dict)
|