Compare commits

...
8 Commits
Author SHA1 Message Date
Ruslan Kuprieiev 9622157565 deps: bump dvc-data to >=2.22.5 (#10141) 2023-12-06 13:32:37 +02:00
Ruslan Kuprieiev 6ff4342f02 deps: bump dvc-data to >=2.22.4 (#10139)
* deps: bump dvc-data to >=2.22.4

* Update test_odb.py
2023-12-06 11:51:05 +02:00
Matt Seddon 5bb72b36cb plots: standardise across DVC/Studio/VS Code (#9931)
* standardise plot data output

* add data required for new anchors to renderer properties

* feed dvc-render updates into API output

* update --json --split api with new data types (send split anchor definitions as strings)

* accomodate zoom and pan anchor

* switch anchor_revs to revs_with_datapoints

* accomodate height and width anchors

* drop terrible idea of holding all data as strings

* fix plot having multiple x fields

* move inferred properties off vega converter class
2023-12-06 11:25:13 +11:00
Ruslan Kuprieiev 1aed230cd0 deps: bump dvc-oss to >=2.22.0
First release with unpinned fsspec version.
2023-12-05 18:48:36 +02:00
pre-commit-ci[bot] c50b2e1e71 [pre-commit.ci] pre-commit autoupdate (#10100)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.5 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.5...v0.1.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-12-05 21:36:22 +05:45
dependabot[bot] 34bc410075 build(deps-dev): bump mypy from 1.7.0 to 1.7.1 (#10108)
Bumps [mypy](https://github.com/python/mypy) from 1.7.0 to 1.7.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 21:23:17 +05:45
47ff40da0c studio: Add Studio authentication to DVC (#10074)
* Add Studio authentication to DVC

Implemented a new feature to authenticate DVC with Iterative Studio. This includes generating an access token, starting device login, checking if user code is authorized, and handling any request exceptions. Furthermore, the commit creates a new authentication command for DVC. This feature was added to allow DVC users to authenticate with Studio, which is necessary for functionality such as sharing live experiments and notifying Studio about pushed experiments.

* Add authentication validation and error handling to DVC Studio

This commit introduces validation for scopes used in DVC Studio's authentication process. If an unsupported scope is passed, the user will receive an error message listing the invalid scopes and the program will return an error status. Additionally, this commit changes the program's exit behavior when device code authorization fails or authentication process is successful, replacing 'sys.exit' with appropriate returned values.

* Add logout functionality to DVC Studio commands

This commit introduces the logout command for DVC Studio. It allows users to manually log out from DVC Studio via the command line interface. The logout command only removes the user's token if it exists, otherwise, it will print an error message and return an error status. This adds an extra layer of control for the users and enhances the security for the usage of DVC Studio.

* Improve User Authentication process for DVC Studio

Implemented a new 'token' command facilitating users to check their authentication token while operating with DVC Studio. This implementation enhances user experience by providing the ability to validate logged-in status. If not logged in, it rightly prompts an error, ensuring secure access.

* Add unit tests for auth commands

Unit tests are added for the authentication commands used in DVC Studio. The modifications include tests for login with invalid scope, standard login, logout, and token commands. The tests verify successful execution and appropriate responses for different user authentication scenarios, thereby increasing the robustness and reliability of the auth module.

* Update unit tests for auth commands

Unit tests for DVC Studio authentication commands have been updated. These checks verify successful execution and appropriate responses under various authentication scenarios, including login with invalid scope, standard login, logout, and token commands in order to enhance the robustness and reliability of the auth module.

* Add unit tests and modify authentication function in studio utils

This commit adds several new unit tests in test_studio.py. Tests for start_device_login and check_token_authorization functionality are included to verify the correct behavior of these methods. Moreover, it also modifies the authentication function in studio.py for improving the robustness. Changes in the function aimed to handle unexpected error scenarios and make the code more resilient.

* Refactor mock_response usage in tests and add test_auth.py

This commit refactors the way mock responses are used in test_studio.py to follow DRY principles, changing the private _mock_response to a public reusable function. It also adds a new file, test_auth.py, that provides functional tests for authorisation methods, ensuring their functionality. The patches in test_auth.py helps test different cases like expired authorization and successful authorization. Furthermore, test_auth.py also tests if correct configuration is set on successful authorization. Modifications on the method test_auth_login_arguments in test_auth.py were made to refine test cases, thus making the tests more precise and straightforward.

* Refactor error handling in `dvc/utils/studio.py`

Refactored error handling by removing redundant code blocks and introducing more detailed logging for request exceptions. This makes the failure cases handled explicitly and streamlines the traceability. Also, tests for authorization have been refined and more specific cases like 'expired authorization' have been included.

* Replace `auth` module with `studio` in DVC

Renamed all instances of `auth` to `studio`, as we are moving the authentication process into a new module `studio`. This includes changes to command names, classes, and test file names to reflect the updated module. It improves the organization of the authentication features related to DVC Studio, separating them from other types of authorization.

* Remove device login methods in utils/studio

Removed methods related to device login and token authorization in `utils/studio.py` and its corresponding test cases. These functionalities have been moved to the `dvc-studio-client` module to decouple the login process and make the code maintainable. Updated the dependencies in `pyproject.toml` to use the new `dvc-studio-client` module. Also, updated the command/studio to use the `dvc-studio-client` module for auth functionalities.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update dvc-studio-client dependency in pyproject.toml

The "dvc-studio-client" dependency in pyproject.toml is updated from a specific git commit to v0.16.0, removing the TODO comment. This change was made because the necessary features from the PR linked in the TODO comment have been merged and released in v0.16.0, making the git commit unnecessary. Now the code is cleaner and easier to track with versioned dependency.

* Update DVC Studio components and simplify authentication process

This commit modifies several parts of the ‘dvc/commands/studio.py’ and updates DVC Studio Client's version in 'pyproject.toml' for compatibility with the changes.

Primarily, the authentication process has been simplified. The unnecessary generation of a random token name is removed while initiating authorization in the run method of CmdStudioLogin class, enhancing the code quality.

Changes made in 'dvc/commands/studio.py' are mainly restructuring the code to adapt to the newer version of DVC Studio Client, including changes to the scopes and methods used from this client.

The corresponding changes were also reflected in unit tests. The test files 'tests/func/test_studio.py' and 'tests/unit/command/test_studio.py' are updated in accordance with the approach now used in 'dvc/commands/studio.py'.

* Remove unnecessary tests and update StudioClient methods

Removed functional test file `test_studio.py`from DVC to move it to studio client. The new mock response no longer needs a full testing file, simplifying our test environment.

In `dvc/commands/studio.py`, adjusted function imports to align with new StudioClient function names, which better indicate their purpose: `initiate_authorization` changed to `get_access_token`.

Additionally, `DEFAULT_SCOPES` was removed from both `dvc/commands/studio.py` and `tests/unit/command/test_studio.py`, simplifying the token acquisition process and removing unnecessary global variable. The `--scopes` command, if needed, can be passed directly without requiring a default fallback.

The corresponding changes were reflected in unit tests : the mock functions in `tests/unit/command/test_studio.py` were changed to align with the new authentication method, and updated the usage of the `get_access_token` function.

* Update token success message in dvc/commands/studio.py

This commit replaces f-string (formatted string literal) with a regular string for the success message after token authentication. The change was necessary as the variable `token_name` is already being passed as a formatted string, thus no need for f-string in the success message sentence. This makes code cleaner and more consistent.

* Refactor tests in `test_studio.py` for better readability

* Updated terminology in `studio.py` and `test_studio.py`

Changes terminology from 'authorize' to 'authenticate' to better align with common practices and improve understanding. Also fixed the `get_access_token` function in 'test_studio.py' where 'AuthorizationExpired' was replaced with 'AuthenticationExpired' for better error handling.

* Change 'dvc' to 'DVC' in `studio.py`

Updates all instances of 'dvc' to 'DVC' in `studio.py` for enhanced consistency with the rest of the project. This change aims to standardize the usage of the term across all project files.

* Improve Studio commands information clarity

Updated descriptions in various commands on 'dvc/commands/studio.py' focused on improving clarity and context. Authentication message, command helper, and descriptions have been expanded to provide better understanding. Also, 'dvc' has been changed to 'DVC' in 'studio.py' for consistency with the rest of the project files.

* Not relevant anymore

* Change argument name in studio login

Renamed the argument from "--use-device-code" to "--no-open" in the login function under the Studio authentication. The option's name was changed to make it more intuitive and better align with its actual effect, avoiding potential user confusion. Corresponding changes have also been made in unit tests.

* Modify flag for Studio login function

Updated the argument name from "-d" to "-o" in `studio.py` for the Studio login function. The change makes the argument's intention clearer for users. This is significant in improving user interaction with the feature and mitigating potential user misinterpretation.

* Apply suggestions from code review

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: skshetry <[email protected]>
2023-12-05 16:30:10 +05:45
Peter Rowlands ee35ec10a2 cache migrate: include dvc.lock dependencies in --dvc-files 2023-12-05 15:42:26 +09:00
18 changed files with 786 additions and 422 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ repos:
- id: sort-simple-yaml
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.1.5'
rev: 'v0.1.6'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
+2
View File
@@ -42,6 +42,7 @@ from dvc.commands import (
repro,
root,
stage,
studio,
unprotect,
update,
version,
@@ -93,6 +94,7 @@ COMMANDS = [
check_ignore,
data,
artifacts,
studio,
]
+1 -45
View File
@@ -59,49 +59,6 @@ def _show_json(
ui.write_json(compact({"errors": all_errors, "data": data}), highlight=False)
def _adjust_vega_renderers(renderers):
from dvc.render import REVISION_FIELD, VERSION_FIELD
from dvc_render import VegaRenderer
for r in renderers:
if isinstance(r, VegaRenderer):
if _data_versions_count(r) > 1:
summary = _summarize_version_infos(r)
for dp in r.datapoints:
vi = dp.pop(VERSION_FIELD, {})
keys = list(vi.keys())
for key in keys:
if not (len(summary.get(key, set())) > 1):
vi.pop(key)
if vi:
dp["rev"] = "::".join(vi.values())
else:
for dp in r.datapoints:
dp[REVISION_FIELD] = dp[VERSION_FIELD]["revision"]
dp.pop(VERSION_FIELD, {})
def _summarize_version_infos(renderer):
from collections import defaultdict
from dvc.render import VERSION_FIELD
result = defaultdict(set)
for dp in renderer.datapoints:
for key, value in dp.get(VERSION_FIELD, {}).items():
result[key].add(value)
return dict(result)
def _data_versions_count(renderer):
from itertools import product
summary = _summarize_version_infos(renderer)
x = product(summary.get("filename", {None}), summary.get("field", {None}))
return len(set(x))
class CmdPlots(CmdBase):
def _func(self, *args, **kwargs):
raise NotImplementedError
@@ -175,11 +132,10 @@ class CmdPlots(CmdBase):
return 0
renderers = [r.renderer for r in renderers_with_errors]
_adjust_vega_renderers(renderers)
if self.args.show_vega:
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
if renderer:
ui.write_json(renderer.get_filled_template(as_string=False))
ui.write_json(renderer.get_filled_template())
return 0
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
+175
View File
@@ -0,0 +1,175 @@
import argparse
import os
from funcy import get_in
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.commands.config import CmdConfig
from dvc.log import logger
logger = logger.getChild(__name__)
class CmdStudioLogin(CmdConfig):
def run(self):
from dvc_studio_client.auth import StudioAuthError, get_access_token
from dvc.env import DVC_STUDIO_URL
from dvc.ui import ui
from dvc.utils.studio import STUDIO_URL
name = self.args.name
hostname = self.args.hostname or os.environ.get(DVC_STUDIO_URL) or STUDIO_URL
scopes = self.args.scopes
try:
token_name, access_token = get_access_token(
token_name=name,
hostname=hostname,
scopes=scopes,
use_device_code=self.args.no_open,
client_name="DVC",
)
except StudioAuthError as e:
ui.error_write(str(e))
return 1
self.save_config(hostname, access_token)
ui.write(
"Authentication has been successfully completed."
"The generated token will now be accessible as"
f" {token_name} in the user's Studio profile."
)
return 0
def save_config(self, hostname, token):
with self.config.edit("global") as conf:
conf["studio"]["token"] = token
conf["studio"]["url"] = hostname
class CmdStudioLogout(CmdConfig):
def run(self):
from dvc.ui import ui
with self.config.edit("global") as conf:
if not get_in(conf, ["studio", "token"]):
ui.error_write("Not logged in to Studio.")
return 1
del conf["studio"]["token"]
ui.write("Logged out from Studio")
return 0
class CmdStudioToken(CmdConfig):
def run(self):
from dvc.ui import ui
conf = self.config.read("global")
token = get_in(conf, ["studio", "token"])
if not token:
ui.error_write("Not logged in to Studio.")
return 1
ui.write(token)
return 0
def add_parser(subparsers, parent_parser):
STUDIO_HELP = "Commands to authenticate DVC with Iterative Studio"
STUDIO_DESCRIPTION = (
"Authenticate DVC with Studio and set the token."
" Once this token has been properly configured,\n"
" DVC will utilize it for seamlessly sharing live experiments\n"
" and sending notifications to Studio regarding any experiments"
" that have been pushed."
)
studio_parser = subparsers.add_parser(
"studio",
parents=[parent_parser],
description=append_doc_link(STUDIO_DESCRIPTION, "studio"),
help=STUDIO_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
studio_subparser = studio_parser.add_subparsers(
dest="cmd",
help="Use `DVC studio CMD --help` to display command-specific help.",
)
fix_subparsers(studio_subparser)
STUDIO_LOGIN_HELP = "Authenticate DVC with Studio host"
STUDIO_LOGIN_DESCRIPTION = (
"By default, this command authenticates the DVC with Studio\n"
" using default scopes and assigns a random name as the token name."
)
login_parser = studio_subparser.add_parser(
"login",
parents=[parent_parser],
description=append_doc_link(STUDIO_LOGIN_DESCRIPTION, "studio/login"),
help=STUDIO_LOGIN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
login_parser.add_argument(
"-H",
"--hostname",
action="store",
default=None,
help="The hostname of the Studio instance to authenticate with.",
)
login_parser.add_argument(
"-s",
"--scopes",
action="store",
default=None,
help="The scopes for the authentication token. ",
)
login_parser.add_argument(
"-n",
"--name",
action="store",
default=None,
help="The name of the authentication token. It will be used to\n"
"identify token shown in Studio profile.",
)
login_parser.add_argument(
"--no-open",
action="store_true",
default=False,
help="Use authentication flow based on user code.\n"
"You will be presented with user code to enter in browser.\n"
"DVC will also use this if it cannot launch browser on your behalf.",
)
login_parser.set_defaults(func=CmdStudioLogin)
STUDIO_LOGOUT_HELP = "Logout user from Studio"
STUDIO_LOGOUT_DESCRIPTION = (
"This removes the studio token from your global config.\n"
)
logout_parser = studio_subparser.add_parser(
"logout",
parents=[parent_parser],
description=append_doc_link(STUDIO_LOGOUT_DESCRIPTION, "studio/logout"),
help=STUDIO_LOGOUT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
logout_parser.set_defaults(func=CmdStudioLogout)
STUDIO_TOKEN_HELP = "View the token dvc uses to contact Studio" # noqa: S105 # nosec B105
logout_parser = studio_subparser.add_parser(
"token",
parents=[parent_parser],
description=append_doc_link(STUDIO_TOKEN_HELP, "studio/token"),
help=STUDIO_TOKEN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
logout_parser.set_defaults(func=CmdStudioToken)
+7 -6
View File
@@ -1,7 +1,8 @@
INDEX_FIELD = "step"
REVISION_FIELD = "rev"
FILENAME_FIELD = "filename"
VERSION_FIELD = "dvc_data_version_info"
REVISIONS_KEY = "revisions"
INDEX = "step"
REVISION = "rev"
FILENAME = "filename"
FIELD = "field"
REVISIONS = "revisions"
ANCHOR_DEFINITIONS = "anchor_definitions"
TYPE_KEY = "type"
SRC_FIELD = "src"
SRC = "src"
+18 -27
View File
@@ -1,7 +1,6 @@
from collections import defaultdict
from typing import Dict, List, Union
from dvc.render import REVISION_FIELD, REVISIONS_KEY, SRC_FIELD, TYPE_KEY, VERSION_FIELD
from dvc.render import REVISION, REVISIONS, SRC, TYPE_KEY
from dvc.render.converter.image import ImageConverter
from dvc.render.converter.vega import VegaConverter
@@ -19,39 +18,31 @@ def _get_converter(
raise ValueError(f"Invalid renderer class {renderer_class}")
def _group_by_rev(datapoints):
grouped = defaultdict(list)
for datapoint in datapoints:
rev = datapoint.get(VERSION_FIELD, {}).get("revision")
grouped[rev].append(datapoint)
return dict(grouped)
def to_json(renderer, split: bool = False) -> List[Dict]:
if renderer.TYPE == "vega":
grouped = _group_by_rev(renderer.datapoints)
if not renderer.datapoints:
return []
revs = renderer.get_revs()
if split:
content = renderer.get_filled_template(
skip_anchors=["data"], as_string=False
)
content, split_content = renderer.get_partial_filled_template()
else:
content = renderer.get_filled_template(as_string=False)
if grouped:
return [
{
TYPE_KEY: renderer.TYPE,
REVISIONS_KEY: sorted(grouped.keys()),
"content": content,
"datapoints": grouped,
}
]
return []
content = renderer.get_filled_template()
split_content = {}
return [
{
TYPE_KEY: renderer.TYPE,
REVISIONS: revs,
"content": content,
**split_content,
}
]
if renderer.TYPE == "image":
return [
{
TYPE_KEY: renderer.TYPE,
REVISIONS_KEY: [datapoint.get(REVISION_FIELD)],
"url": datapoint.get(SRC_FIELD),
REVISIONS: [datapoint.get(REVISION)],
"url": datapoint.get(SRC),
}
for datapoint in renderer.datapoints
]
+4 -4
View File
@@ -2,7 +2,7 @@ import base64
import os
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
from dvc.render import FILENAME_FIELD, REVISION_FIELD, SRC_FIELD
from dvc.render import FILENAME, REVISION, SRC
from . import Converter
@@ -58,9 +58,9 @@ class ImageConverter(Converter):
else:
src = self._encode_image(image_data)
datapoint = {
REVISION_FIELD: revision,
FILENAME_FIELD: filename,
SRC_FIELD: src,
REVISION: revision,
FILENAME: filename,
SRC: src,
}
datapoints.append(datapoint)
return datapoints, properties
+41 -26
View File
@@ -4,7 +4,7 @@ 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 dvc.render import FIELD, FILENAME, INDEX, REVISION
from . import Converter
@@ -112,7 +112,6 @@ class VegaConverter(Converter):
):
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:
@@ -120,34 +119,38 @@ class VegaConverter(Converter):
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
return {self.plot_id: field}
return None
def _infer_x_y(self):
x = self.properties.get("x", None)
y = self.properties.get("y", None)
inferred_properties: Dict = {}
# Infer x.
if isinstance(x, str):
self.inferred_properties["x"] = {}
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)
inferred_properties["x"][file] = [x] * len(fields)
else:
self.inferred_properties["x"][file] = x
inferred_properties["x"][file] = x
# Otherwise use plot ID as file.
else:
self.inferred_properties["x"][self.plot_id] = x
inferred_properties["x"][self.plot_id] = x
# Infer y.
if y is None:
self._infer_y_from_data()
inferred_properties["y"] = 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}
inferred_properties["y"] = {self.plot_id: y}
return inferred_properties
def _find_datapoints(self):
result = {}
@@ -182,7 +185,7 @@ class VegaConverter(Converter):
x = properties.get("x", None)
if not isinstance(x, dict):
return INDEX_FIELD
return INDEX
fields = {field for _, field in _file_field(x)}
if len(fields) == 1:
@@ -192,7 +195,7 @@ class VegaConverter(Converter):
def flat_datapoints(self, revision): # noqa: C901, PLR0912
file2datapoints, properties = self.convert()
props_update = {}
props_update: Dict[str, Union[str, List[Dict[str, str]]]] = {}
xs = list(_get_xs(properties, file2datapoints))
@@ -200,15 +203,17 @@ class VegaConverter(Converter):
if not xs:
x_file, x_field = (
None,
INDEX_FIELD,
INDEX,
)
else:
x_file, x_field = xs[0]
props_update["x"] = x_field
num_xs = len(xs)
multiple_x_fields = num_xs > 1 and len({x[1] for x in xs}) > 1
props_update["x"] = "dvc_inferred_x_value" if multiple_x_fields else 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(
@@ -237,6 +242,14 @@ class VegaConverter(Converter):
else:
common_prefix_len = 0
props_update["anchors_y_definitions"] = [
{
FILENAME: _get_short_y_file(y_file, common_prefix_len),
FIELD: y_field,
}
for y_file, y_field in ys
]
for i, (y_file, y_field) in enumerate(ys):
if num_xs > 1:
x_file, x_field = xs[i]
@@ -249,15 +262,16 @@ class VegaConverter(Converter):
source_field=y_field,
)
if x_field == INDEX_FIELD and x_file is None:
_update_from_index(datapoints, INDEX_FIELD)
if x_field == INDEX and x_file is None:
_update_from_index(datapoints, INDEX)
else:
x_datapoints = file2datapoints.get(x_file, [])
try:
_update_from_field(
datapoints,
field=x_field,
field="dvc_inferred_x_value" if multiple_x_fields else x_field,
source_datapoints=x_datapoints,
source_field=x_field,
)
except IndexError:
raise DvcException( # noqa: B904
@@ -266,15 +280,12 @@ class VegaConverter(Converter):
"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,
}
REVISION: revision,
FILENAME: _get_short_y_file(y_file, common_prefix_len),
FIELD: y_field,
},
)
@@ -295,10 +306,10 @@ class VegaConverter(Converter):
generated datapoints and updated properties. `x`, `y` values and labels
are inferred and always provided.
"""
self._infer_x_y()
inferred_properties = self._infer_x_y()
datapoints = self._find_datapoints()
properties = {**self.properties, **self.inferred_properties}
properties = {**self.properties, **inferred_properties}
properties["y_label"] = self.infer_y_label(properties)
properties["x_label"] = self.infer_x_label(properties)
@@ -306,6 +317,10 @@ class VegaConverter(Converter):
return datapoints, properties
def _get_short_y_file(y_file, common_prefix_len):
return y_file[common_prefix_len:].strip("/\\")
def _update_from_field(
target_datapoints: List[Dict],
field: str,
+12 -6
View File
@@ -80,7 +80,7 @@ def match_defs_renderers( # noqa: C901, PLR0912
for plot_id, group in plots_data.group_definitions().items():
plot_datapoints: List[Dict] = []
props = _squash_plots_properties(group)
final_props: Dict = {}
first_props: Dict = {}
def_errors: Dict[str, Exception] = {}
src_errors: DefaultDict[str, Dict[str, Exception]] = defaultdict(dict)
@@ -90,6 +90,7 @@ def match_defs_renderers( # noqa: C901, PLR0912
if templates_dir is not None:
props["template_dir"] = templates_dir
revs = []
for rev, inner_id, plot_definition in group:
plot_sources = infer_data_sources(inner_id, plot_definition)
definitions_data = plots_data.get_definition_data(plot_sources, rev)
@@ -109,19 +110,24 @@ def match_defs_renderers( # noqa: C901, PLR0912
try:
dps, rev_props = converter.flat_datapoints(rev)
if dps and rev not in revs:
revs.append(rev)
except Exception as e: # noqa: BLE001
logger.warning("In %r, %s", rev, str(e).lower())
def_errors[rev] = e
continue
if not final_props and rev_props:
final_props = rev_props
if not first_props and rev_props:
first_props = rev_props
plot_datapoints.extend(dps)
if "title" not in final_props:
final_props["title"] = renderer_id
if "title" not in first_props:
first_props["title"] = renderer_id
if revs:
first_props["revs_with_datapoints"] = revs
if renderer_cls is not None:
renderer = renderer_cls(plot_datapoints, renderer_id, **final_props)
renderer = renderer_cls(plot_datapoints, renderer_id, **first_props)
renderers.append(RendererWithErrors(renderer, dict(src_errors), def_errors))
return renderers
+33 -9
View File
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Set
from dvc import prompt
@@ -7,6 +7,7 @@ from .scm_context import scm_context
if TYPE_CHECKING:
from . import Repo
from .index import IndexView
def _prepare_message(stage, changes):
@@ -88,14 +89,7 @@ def commit_2_to_3(repo: "Repo", dry: bool = False):
outs_filter=lambda o: o.hash_name == "md5-dos2unix",
recursive=True,
)
migrated = set()
for out in view.outs:
dvcfile = out.stage.dvcfile.relpath
if isinstance(out.stage.dvcfile, ProjectFile):
lockfile = out.stage.dvcfile._lockfile.relpath
migrated.add(f"{dvcfile} ({lockfile})")
else:
migrated.add(dvcfile)
migrated = _migrateable_dvcfiles(view)
if not migrated:
ui.write("No DVC files in the repo to migrate to the 3.0 format.")
return
@@ -110,11 +104,41 @@ def commit_2_to_3(repo: "Repo", dry: bool = False):
for out in stage.filter_outs(filter_info)
if outs_filter is not None and outs_filter(out)
}
modified = False
if outs:
for out in outs:
out.update_legacy_hash_name(force=True)
modified = True
deps = {dep for dep in stage.deps if not stage.is_import and dep.is_in_repo}
if deps:
for dep in deps:
dep.update_legacy_hash_name(force=True)
modified = True
if modified:
stage.save(allow_missing=True)
stage.commit(allow_missing=True, relink=True)
if not isinstance(stage.dvcfile, ProjectFile):
ui.write(f"Updating DVC file '{stage.dvcfile.relpath}'")
stage.dump(update_pipeline=False)
def _migrateable_dvcfiles(view: "IndexView") -> Set[str]:
from dvc.dvcfile import ProjectFile
migrated = set()
for stage, filter_info in view._stage_infos:
outs_filter = view._outs_filter
dvcfile = stage.dvcfile.relpath
assert outs_filter
if any(outs_filter(out) for out in stage.filter_outs(filter_info)) or (
not stage.is_import
and any(
dep.is_in_repo and dep.hash_name == "md5-dos2unix" for dep in stage.deps
)
):
if isinstance(stage.dvcfile, ProjectFile):
lockfile = stage.dvcfile._lockfile.relpath
migrated.add(f"{dvcfile} ({lockfile})")
else:
migrated.add(dvcfile)
return migrated
+6 -5
View File
@@ -29,15 +29,16 @@ classifiers = [
"Programming Language :: Python :: 3.11",
]
dynamic = ["version"]
dependencies = [
"colorama>=0.3.9",
"configobj>=5.0.6",
"distro>=1.3",
"dpath<3,>=2.1.0",
"dvc-data>=2.22.1,<2.23.0",
"dvc-data>=2.22.5,<2.23.0",
"dvc-http>=2.29.0",
"dvc-render>=0.3.1,<1",
"dvc-studio-client>=0.13.0,<1",
"dvc-render>=1.0.0,<2",
"dvc-studio-client>=0.17.1,<1",
"dvc-task>=0.3.0,<1",
"flatten_dict<1,>=0.4.1",
# https://github.com/iterative/dvc/issues/9654
@@ -76,7 +77,7 @@ gdrive = ["dvc-gdrive==2.20"]
gs = ["dvc-gs==2.22.1"]
hdfs = ["dvc-hdfs==2.19"]
lint = [
"mypy==1.7.0",
"mypy==1.7.1",
"pandas-stubs",
"types-colorama",
"types-psutil",
@@ -86,7 +87,7 @@ lint = [
"types-toml",
"types-tqdm",
]
oss = ["dvc-oss>=2.20.0"]
oss = ["dvc-oss>=2.22.0"]
s3 = ["dvc-s3==2.23.0"]
ssh = ["dvc-ssh>=2.22.1,<3"]
ssh_gssapi = ["dvc-ssh[gssapi]>=2.22.1,<3"]
+2 -2
View File
@@ -165,7 +165,7 @@ def test_default_cache_type(dvc):
@pytest.mark.skipif(os.name == "nt", reason="Not supported for Windows.")
@pytest.mark.parametrize("group", [False, True])
def test_shared_cache(tmp_dir, dvc, group):
from dvc.fs import system
from dvc_data.hashfile.db.local import umask
if group:
with dvc.config.edit() as conf:
@@ -178,7 +178,7 @@ def test_shared_cache(tmp_dir, dvc, group):
tmp_dir.dvc_gen({"file": "file content", "dir": {"file2": "file 2 content"}})
file_mode = oct(0o444)
dir_mode = oct(0o2775 if group else (0o777 & ~system.umask))
dir_mode = oct(0o2775 if group else (0o777 & ~umask))
for root, dnames, fnames in os.walk(cache_dir):
for dname in dnames:
path = os.path.join(root, dname)
+93 -58
View File
@@ -11,7 +11,7 @@ from bs4 import BeautifulSoup
from funcy import first
from dvc.cli import main
from dvc.render import REVISION_FIELD, VERSION_FIELD
from dvc.render import ANCHOR_DEFINITIONS, FILENAME, REVISION
JSON_OUT = "vis_data"
@@ -102,6 +102,9 @@ def verify_vega(
html_result,
json_result,
split_json_result,
title,
x_label,
y_label,
):
if isinstance(versions, str):
versions = [versions]
@@ -111,27 +114,60 @@ def verify_vega(
assert j[0]["type"] == "vega"
assert set(j[0]["revisions"]) == set(versions)
assert json_result[0]["datapoints"] == split_json_result[0]["datapoints"]
assert set(versions) == set(json_result[0]["datapoints"].keys())
assert (
json_result[0]["content"]["data"]["values"]
== split_json_result[0][ANCHOR_DEFINITIONS]["<DVC_METRIC_DATA>"]
)
assert set(versions) == set(json_result[0]["revisions"])
assert json_result[0]["content"]["data"]["values"]
assert html_result["data"]["values"]
assert split_json_result[0]["content"]["data"]["values"] == "<DVC_METRIC_DATA>"
def _assert_templates_equal(html_template, filled_template, split_template):
# besides data, json and split json should be equal
path = ["data", "values"]
content_str = json.dumps(split_json_result[0]["content"])
assert "<DVC_METRIC_DATA>" in content_str
assert "<DVC_METRIC_X_LABEL>" in content_str
assert "<DVC_METRIC_Y_LABEL>" in content_str
def _assert_templates_equal(
html_template, filled_template, split_template, title, x_label, y_label
):
# besides split anchors, json and split json should be equal
paths = [["data", "values"], ["encoding", "color"]]
tmp1 = deepcopy(html_template)
tmp2 = deepcopy(filled_template)
tmp3 = deepcopy(split_template)
dpath.set(tmp1, path, {})
dpath.set(tmp2, path, {})
dpath.set(tmp3, path, {})
tmp3 = json.loads(
json.dumps(split_template)
.replace('"<DVC_METRIC_PLOT_HEIGHT>"', "300")
.replace('"<DVC_METRIC_PLOT_WIDTH>"', "300")
.replace("<DVC_METRIC_TITLE>", title)
.replace("<DVC_METRIC_X_LABEL>", x_label)
.replace("<DVC_METRIC_Y_LABEL>", y_label)
.replace(
'"<DVC_METRIC_ZOOM_AND_PAN>"',
json.dumps(
{
"name": "grid",
"select": "interval",
"bind": "scales",
}
),
)
)
for path in paths:
dpath.set(tmp1, path, {})
dpath.set(tmp2, path, {})
dpath.set(tmp3, path, {})
assert tmp1 == tmp2 == tmp3
_assert_templates_equal(
html_result, json_result[0]["content"], split_json_result[0]["content"]
html_result,
json_result[0]["content"],
split_json_result[0]["content"],
title,
x_label,
y_label,
)
@@ -193,17 +229,13 @@ def test_repo_with_plots(tmp_dir, scm, dvc, capsys, run_copy_metrics, repo_with_
] == _update_datapoints(
linear_v1,
{
VERSION_FIELD: {
"revision": "workspace",
"filename": "linear.json",
"field": "y",
},
REVISION: "workspace",
},
)
assert html_result["linear.json"]["data"]["values"] == _update_datapoints(
linear_v1,
{
REVISION_FIELD: "workspace",
REVISION: "workspace",
},
)
assert json_data["confusion.json"][0]["content"]["data"][
@@ -211,27 +243,29 @@ def test_repo_with_plots(tmp_dir, scm, dvc, capsys, run_copy_metrics, repo_with_
] == _update_datapoints(
confusion_v1,
{
VERSION_FIELD: {
"revision": "workspace",
"filename": "confusion.json",
"field": "actual",
},
REVISION: "workspace",
},
)
assert html_result["confusion.json"]["data"]["values"] == _update_datapoints(
confusion_v1,
{
REVISION_FIELD: "workspace",
REVISION: "workspace",
},
)
verify_image(tmp_dir, "workspace", "image.png", image_v1, html_path, json_data)
for plot in ["linear.json", "confusion.json"]:
for plot, title, x_label, y_label in [
("linear.json", "linear", "x", "y"),
("confusion.json", "confusion matrix", "predicted", "actual"),
]:
verify_vega(
"workspace",
html_result[plot],
json_data[plot],
split_json_data[plot],
title,
x_label,
y_label,
)
verify_vega_props("confusion.json", json_data, **confusion_props)
@@ -250,12 +284,18 @@ def test_repo_with_plots(tmp_dir, scm, dvc, capsys, run_copy_metrics, repo_with_
verify_image(tmp_dir, "workspace", "image.png", image_v2, html_path, json_data)
verify_image(tmp_dir, "HEAD", "image.png", image_v1, html_path, json_data)
for plot in ["linear.json", "confusion.json"]:
for plot, title, x_label, y_label in [
("linear.json", "linear", "x", "y"),
("confusion.json", "confusion matrix", "predicted", "actual"),
]:
verify_vega(
["HEAD", "workspace"],
html_result[plot],
json_data[plot],
split_json_data[plot],
title,
x_label,
y_label,
)
verify_vega_props("confusion.json", json_data, **confusion_props)
path = tmp_dir / "subdir"
@@ -277,31 +317,23 @@ def test_repo_with_plots(tmp_dir, scm, dvc, capsys, run_copy_metrics, repo_with_
] == _update_datapoints(
linear_v2,
{
VERSION_FIELD: {
"revision": "workspace",
"filename": "../linear.json",
"field": "y",
},
REVISION: "workspace",
},
) + _update_datapoints(
linear_v1,
{
VERSION_FIELD: {
"revision": "HEAD",
"filename": "../linear.json",
"field": "y",
},
REVISION: "HEAD",
},
)
assert html_result["../linear.json"]["data"]["values"] == _update_datapoints(
linear_v2,
{
REVISION_FIELD: "workspace",
REVISION: "workspace",
},
) + _update_datapoints(
linear_v1,
{
REVISION_FIELD: "HEAD",
REVISION: "HEAD",
},
)
assert json_data["../confusion.json"][0]["content"]["data"][
@@ -309,43 +341,38 @@ def test_repo_with_plots(tmp_dir, scm, dvc, capsys, run_copy_metrics, repo_with_
] == _update_datapoints(
confusion_v2,
{
VERSION_FIELD: {
"revision": "workspace",
"filename": "../confusion.json",
"field": "actual",
},
REVISION: "workspace",
},
) + _update_datapoints(
confusion_v1,
{
VERSION_FIELD: {
"revision": "HEAD",
"filename": "../confusion.json",
"field": "actual",
},
REVISION: "HEAD",
},
)
assert html_result["../confusion.json"]["data"]["values"] == _update_datapoints(
confusion_v2,
{
REVISION_FIELD: "workspace",
REVISION: "workspace",
},
) + _update_datapoints(
confusion_v1,
{
REVISION_FIELD: "HEAD",
REVISION: "HEAD",
},
)
for plot in [
"../linear.json",
"../confusion.json",
for plot, title, x_label, y_label in [
("../linear.json", "linear", "x", "y"),
("../confusion.json", "confusion matrix", "predicted", "actual"),
]:
verify_vega(
["HEAD", "workspace"],
html_result[plot],
json_data[plot],
split_json_data[plot],
title,
x_label,
y_label,
)
verify_image(
path,
@@ -434,7 +461,7 @@ def test_repo_with_config_plots(tmp_dir, capsys, repo_with_config_plots):
repo_state = repo_with_config_plots()
plots = next(repo_state)
html_path, _, __ = call(capsys)
html_path, _, split_json_result = call(capsys)
assert os.path.exists(html_path)
html_result = extract_vega_specs(
@@ -444,20 +471,28 @@ def test_repo_with_config_plots(tmp_dir, capsys, repo_with_config_plots):
"confusion_train_vs_test",
],
)
ble = _update_datapoints(
plots["data"]["linear_train.json"],
{
REVISION_FIELD: "linear_train.json",
REVISION: "workspace",
FILENAME: "linear_train.json",
},
) + _update_datapoints(
plots["data"]["linear_test.json"],
{
REVISION_FIELD: "linear_test.json",
REVISION: "workspace",
FILENAME: "linear_test.json",
},
)
assert html_result["linear_train_vs_test"]["data"]["values"] == ble
# TODO check json results once vscode is able to handle flexible plots
assert (
split_json_result["data"]["linear_train_vs_test"][0][ANCHOR_DEFINITIONS][
"<DVC_METRIC_DATA>"
]
== ble
)
@pytest.mark.vscode
@@ -466,7 +501,7 @@ def test_repo_with_dvclive_plots(tmp_dir, capsys, repo_with_dvclive_plots):
for s in ("show", "diff"):
_, json_result, split_json_result = call(capsys, subcommand=s)
expected_result = {
expected_result: Dict[str, Dict[str, list[str]]] = {
"data": {
"dvclive/plots/metrics/metric.tsv": [],
},
+81
View File
@@ -0,0 +1,81 @@
from dvc_studio_client.auth import AuthenticationExpired
from dvc.cli import main
from dvc.utils.studio import STUDIO_URL
def test_studio_login_token_check_failed(mocker):
mocker.patch(
"dvc_studio_client.auth.get_access_token", side_effect=AuthenticationExpired
)
assert main(["studio", "login"]) == 1
def test_studio_login_success(mocker, dvc):
mocker.patch(
"dvc_studio_client.auth.get_access_token",
return_value=("token_name", "isat_access_token"),
)
assert main(["studio", "login"]) == 0
config = dvc.config.load_one("global")
assert config["studio"]["token"] == "isat_access_token"
assert config["studio"]["url"] == STUDIO_URL
def test_studio_login_arguments(mocker):
mock = mocker.patch(
"dvc_studio_client.auth.get_access_token",
return_value=("token_name", "isat_access_token"),
)
assert (
main(
[
"studio",
"login",
"--name",
"token_name",
"--hostname",
"https://example.com",
"--scopes",
"experiments",
"--no-open",
]
)
== 0
)
mock.assert_called_with(
token_name="token_name",
hostname="https://example.com",
scopes="experiments",
use_device_code=True,
client_name="DVC",
)
def test_studio_logout(dvc):
with dvc.config.edit("global") as conf:
conf["studio"]["token"] = "isat_access_token"
assert main(["studio", "logout"]) == 0
config = dvc.config.load_one("global")
assert "token" not in config["studio"]
assert main(["studio", "logout"]) == 1
def test_studio_token(dvc, capsys):
with dvc.config.edit("global") as conf:
conf["studio"]["token"] = "isat_access_token"
assert main(["studio", "token"]) == 0
assert capsys.readouterr().out == "isat_access_token\n"
with dvc.config.edit("global") as conf:
del conf["studio"]["token"]
assert main(["studio", "token"]) == 1
+54 -76
View File
@@ -1,109 +1,87 @@
from dvc.render import REVISION_FIELD, REVISIONS_KEY, SRC_FIELD, TYPE_KEY, VERSION_FIELD
import json
import pytest
from dvc.render import ANCHOR_DEFINITIONS, FILENAME, REVISION, REVISIONS, SRC, TYPE_KEY
from dvc.render.convert import to_json
def test_to_json_vega(mocker):
vega_renderer = mocker.MagicMock()
vega_renderer.TYPE = "vega"
vega_renderer.get_revs.return_value = ["bar", "foo"]
vega_renderer.get_filled_template.return_value = {"this": "is vega"}
vega_renderer.datapoints = [
{
"x": 1,
"y": 2,
VERSION_FIELD: {"revision": "foo"},
"filename": "foo.json",
},
{
"x": 2,
"y": 1,
VERSION_FIELD: {"revision": "bar"},
"filename": "foo.json",
},
]
result = to_json(vega_renderer)
assert result[0] == {
TYPE_KEY: vega_renderer.TYPE,
REVISIONS_KEY: ["bar", "foo"],
REVISIONS: ["bar", "foo"],
"content": {"this": "is vega"},
"datapoints": {
"foo": [
{
"x": 1,
"y": 2,
"filename": "foo.json",
VERSION_FIELD: {"revision": "foo"},
},
],
"bar": [
{
"x": 2,
"y": 1,
"filename": "foo.json",
VERSION_FIELD: {"revision": "bar"},
},
],
},
}
vega_renderer.get_filled_template.assert_called()
@pytest.mark.vscode
def test_to_json_vega_split(mocker):
revs = ["bar", "foo"]
content = json.dumps(
{
"this": "is split vega",
"encoding": {"color": "<DVC_METRIC_COLOR>"},
"data": {"values": "<DVC_METRIC_DATA>"},
}
)
anchor_definitions = {
"<DVC_METRIC_COLOR>": {
"field": "rev",
"scale": {
"domain": revs,
"range": ["#ff0000", "#00ff00"],
},
},
"<DVC_METRIC_DATA>": [
{
"x": 1,
"y": 2,
REVISION: "foo",
FILENAME: "foo.json",
},
{
"x": 2,
"y": 1,
REVISION: "bar",
FILENAME: "foo.json",
},
],
}
vega_renderer = mocker.MagicMock()
vega_renderer.TYPE = "vega"
vega_renderer.get_filled_template.return_value = {"this": "is split vega"}
vega_renderer.datapoints = [
{
"x": 1,
"y": 2,
VERSION_FIELD: {"revision": "foo"},
"filename": "foo.json",
},
{
"x": 2,
"y": 1,
VERSION_FIELD: {"revision": "bar"},
"filename": "foo.json",
},
]
vega_renderer.get_partial_filled_template.return_value = (
content,
{ANCHOR_DEFINITIONS: anchor_definitions},
)
vega_renderer.get_revs.return_value = ["bar", "foo"]
result = to_json(vega_renderer, split=True)
assert result[0] == {
ANCHOR_DEFINITIONS: anchor_definitions,
TYPE_KEY: vega_renderer.TYPE,
REVISIONS_KEY: ["bar", "foo"],
"content": {"this": "is split vega"},
"datapoints": {
"foo": [
{
"x": 1,
"y": 2,
"filename": "foo.json",
VERSION_FIELD: {"revision": "foo"},
}
],
"bar": [
{
"x": 2,
"y": 1,
"filename": "foo.json",
VERSION_FIELD: {"revision": "bar"},
}
],
},
REVISIONS: revs,
"content": content,
}
vega_renderer.get_filled_template.assert_called_with(
as_string=False, skip_anchors=["data"]
)
vega_renderer.get_partial_filled_template.assert_called_once()
def test_to_json_image(mocker):
image_renderer = mocker.MagicMock()
image_renderer.TYPE = "image"
image_renderer.datapoints = [
{SRC_FIELD: "contentfoo", REVISION_FIELD: "foo"},
{SRC_FIELD: "contentbar", REVISION_FIELD: "bar"},
{SRC: "contentfoo", REVISION: "foo"},
{SRC: "contentbar", REVISION: "bar"},
]
result = to_json(image_renderer)
assert result[0] == {
"url": image_renderer.datapoints[0].get(SRC_FIELD),
REVISIONS_KEY: [image_renderer.datapoints[0].get(REVISION_FIELD)],
"url": image_renderer.datapoints[0].get(SRC),
REVISIONS: [image_renderer.datapoints[0].get(REVISION)],
TYPE_KEY: image_renderer.TYPE,
}
+10 -10
View File
@@ -1,4 +1,4 @@
from dvc.render import REVISION_FIELD, SRC_FIELD
from dvc.render import FILENAME, REVISION, SRC
from dvc.render.converter.image import ImageConverter
@@ -8,9 +8,9 @@ def test_image_converter_no_out():
datapoints, _ = converter.flat_datapoints("r")
assert datapoints[0] == {
REVISION_FIELD: "r",
"filename": "image.png",
SRC_FIELD: converter._encode_image(b"content"),
REVISION: "r",
FILENAME: "image.png",
SRC: converter._encode_image(b"content"),
}
@@ -21,9 +21,9 @@ def test_image_converter_with_out(tmp_dir):
datapoints, _ = converter.flat_datapoints("r")
assert datapoints[0] == {
REVISION_FIELD: "r",
"filename": "image.png",
SRC_FIELD: str(tmp_dir / "foo" / "r_image.png"),
REVISION: "r",
FILENAME: "image.png",
SRC: str(tmp_dir / "foo" / "r_image.png"),
}
assert (tmp_dir / "foo" / "r_image.png").read_bytes() == b"content"
@@ -37,9 +37,9 @@ def test_image_converter_with_slash_in_revision(tmp_dir):
datapoints, _ = converter.flat_datapoints("feature/r")
assert datapoints[0] == {
REVISION_FIELD: "feature/r",
"filename": "image.png",
SRC_FIELD: str(tmp_dir / "foo" / "feature_r_image.png"),
REVISION: "feature/r",
FILENAME: "image.png",
SRC: str(tmp_dir / "foo" / "feature_r_image.png"),
}
assert (tmp_dir / "foo" / "feature_r_image.png").read_bytes() == b"content"
+14 -12
View File
@@ -1,9 +1,13 @@
import pytest
from funcy import set_in
from dvc.render import VERSION_FIELD
from dvc.render import FIELD, FILENAME, REVISION
from dvc.render.converter.vega import VegaConverter
from dvc.render.match import PlotsData, _squash_plots_properties, match_defs_renderers
from dvc.render.match import (
PlotsData,
_squash_plots_properties,
match_defs_renderers,
)
@pytest.mark.parametrize(
@@ -158,25 +162,23 @@ def test_match_renderers(M):
renderer = renderer_with_errors[0]
assert renderer.datapoints == [
{
VERSION_FIELD: {
"revision": "v1",
"filename": "file.json",
"field": "y",
},
REVISION: "v1",
FILENAME: "file.json",
FIELD: "y",
"x": 1,
"y": 1,
},
{
VERSION_FIELD: {
"revision": "v1",
"filename": "file.json",
"field": "y",
},
REVISION: "v1",
FILENAME: "file.json",
FIELD: "y",
"x": 2,
"y": 2,
},
]
assert renderer.properties == {
"anchors_y_definitions": [{FILENAME: "file.json", FIELD: "y"}],
"revs_with_datapoints": ["v1"],
"title": "plot_id_1",
"x": "x",
"y": "y",
+232 -135
View File
@@ -3,7 +3,7 @@ from collections import OrderedDict
import pytest
from dvc.exceptions import DvcException
from dvc.render import VERSION_FIELD
from dvc.render import FIELD, FILENAME, REVISION
from dvc.render.converter.vega import FieldNotFoundError, VegaConverter, _lists
@@ -35,23 +35,25 @@ def test_finding_lists(dictionary, expected_result):
{
"v": 1,
"step": 0,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
{
"v": 2,
"step": 1,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
],
{"x": "step", "y": "v", "x_label": "step", "y_label": "v"},
{
"anchors_y_definitions": [{FILENAME: "f", FIELD: "v"}],
"x": "step",
"y": "v",
"x_label": "step",
"y_label": "v",
},
id="default_x_y",
),
pytest.param(
@@ -61,23 +63,25 @@ def test_finding_lists(dictionary, expected_result):
{
"v": 1,
"v2": 0.1,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
{
"v": 2,
"v2": 0.2,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
],
{"x": "v", "y": "v2", "x_label": "v", "y_label": "v2"},
{
"anchors_y_definitions": [{FILENAME: "f", FIELD: "v2"}],
"x": "v",
"y": "v2",
"x_label": "v",
"y_label": "v2",
},
id="choose_x_y",
),
pytest.param(
@@ -99,23 +103,25 @@ def test_finding_lists(dictionary, expected_result):
{
"v": 1,
"v2": 0.1,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
{
"v": 2,
"v2": 0.2,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
],
{"x": "v", "y": "v2", "x_label": "x", "y_label": "y"},
{
"anchors_y_definitions": [{FILENAME: "f", FIELD: "v2"}],
"x": "v",
"y": "v2",
"x_label": "x",
"y_label": "y",
},
id="find_in_nested_structure",
),
pytest.param(
@@ -123,44 +129,36 @@ def test_finding_lists(dictionary, expected_result):
{"y": {"f": ["v", "v2"]}},
[
{
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v",
"dvc_inferred_y_value": 1,
"v": 1,
"v2": 0.1,
"step": 0,
},
{
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v",
"dvc_inferred_y_value": 2,
"v": 2,
"v2": 0.2,
"step": 1,
},
{
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
"dvc_inferred_y_value": 0.1,
"v2": 0.1,
"v": 1,
"step": 0,
},
{
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
"v": 2,
"v2": 0.2,
"dvc_inferred_y_value": 0.2,
@@ -168,6 +166,10 @@ def test_finding_lists(dictionary, expected_result):
},
],
{
"anchors_y_definitions": [
{FILENAME: "f", FIELD: "v"},
{FILENAME: "f", FIELD: "v2"},
],
"x": "step",
"y": "dvc_inferred_y_value",
"y_label": "y",
@@ -189,47 +191,43 @@ def test_finding_lists(dictionary, expected_result):
"z": 3,
"v": 1,
"step": 0,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
{
"dvc_inferred_y_value": 2,
"z": 4,
"step": 1,
"v": 2,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
{
"dvc_inferred_y_value": 3,
"v": 1,
"z": 3,
"step": 0,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "z",
},
REVISION: "r",
FILENAME: "f",
FIELD: "z",
},
{
"dvc_inferred_y_value": 4,
"v": 2,
"z": 4,
"step": 1,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "z",
},
REVISION: "r",
FILENAME: "f",
FIELD: "z",
},
],
{
"anchors_y_definitions": [
{FILENAME: "f", FIELD: "v"},
{FILENAME: "f", FIELD: "z"},
],
"x": "step",
"y": "dvc_inferred_y_value",
"y_label": "y",
@@ -247,32 +245,35 @@ def test_finding_lists(dictionary, expected_result):
{
"v": 1,
"v2": 0.1,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
{
"v": 2,
"v2": 0.2,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
{
"v": 3,
"v2": 0.3,
VERSION_FIELD: {
"revision": "r",
"filename": "f2",
"field": "v2",
},
REVISION: "r",
FILENAME: "f2",
FIELD: "v2",
},
],
{"x": "v", "y": "v2", "x_label": "v", "y_label": "v2"},
{
"anchors_y_definitions": [
{FILENAME: "f", FIELD: "v2"},
{FILENAME: "f2", FIELD: "v2"},
],
"x": "v",
"y": "v2",
"x_label": "v",
"y_label": "v2",
},
id="multi_file_json",
),
pytest.param(
@@ -284,47 +285,43 @@ def test_finding_lists(dictionary, expected_result):
"v": 1,
"v2": 0.1,
"step": 0,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
{
"dvc_inferred_y_value": 2,
"v": 2,
"v2": 0.2,
"step": 1,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
{
"dvc_inferred_y_value": 0.1,
"v": 1,
"v2": 0.1,
"step": 0,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
{
"dvc_inferred_y_value": 0.2,
"v": 2,
"v2": 0.2,
"step": 1,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
],
{
"anchors_y_definitions": [
{FILENAME: "f", FIELD: "v"},
{FILENAME: "f", FIELD: "v2"},
],
"x": "step",
"y": "dvc_inferred_y_value",
"x_label": "step",
@@ -344,35 +341,34 @@ def test_finding_lists(dictionary, expected_result):
"v": 1,
"v2": 0.1,
"v3": 0.01,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
{
"dvc_inferred_y_value": 0.01,
"v": 1,
"v2": 0.1,
"v3": 0.01,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v3",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v3",
},
{
"dvc_inferred_y_value": 0.1,
"v": 1,
"v2": 0.1,
VERSION_FIELD: {
"revision": "r",
"filename": "f2",
"field": "v2",
},
REVISION: "r",
FILENAME: "f2",
FIELD: "v2",
},
],
{
"anchors_y_definitions": [
{FILENAME: "f", FIELD: "v2"},
{FILENAME: "f", FIELD: "v3"},
{FILENAME: "f2", FIELD: "v2"},
],
"x": "v",
"y": "dvc_inferred_y_value",
"x_label": "v",
@@ -390,23 +386,23 @@ def test_finding_lists(dictionary, expected_result):
{
"v": 1,
"v2": 0.1,
VERSION_FIELD: {
"revision": "r",
"filename": "f",
"field": "v2",
},
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
{
"v": 1,
"v2": 0.1,
VERSION_FIELD: {
"revision": "r",
"filename": "f2",
"field": "v2",
},
REVISION: "r",
FILENAME: "f2",
FIELD: "v2",
},
],
{
"anchors_y_definitions": [
{FILENAME: "f", FIELD: "v2"},
{FILENAME: "f2", FIELD: "v2"},
],
"x": "v",
"y": "v2",
"x_label": "v",
@@ -414,6 +410,107 @@ def test_finding_lists(dictionary, expected_result):
},
id="multi_file_y_same_prefix",
),
pytest.param(
{
"f": {"metric": [{"x1": 1, "v": 0.1}]},
"f2": {"metric": [{"x2": 100, "v": 0.1}]},
},
{"y": {"f": ["v"], "f2": ["v"]}, "x": {"f": "x1", "f2": "x2"}},
[
{
"x1": 1,
"v": 0.1,
"dvc_inferred_x_value": 1,
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
{
"x2": 100,
"v": 0.1,
"dvc_inferred_x_value": 100,
REVISION: "r",
FILENAME: "f2",
FIELD: "v",
},
],
{
"anchors_y_definitions": [
{FILENAME: "f", FIELD: "v"},
{FILENAME: "f2", FIELD: "v"},
],
"x": "dvc_inferred_x_value",
"y": "v",
"x_label": "x",
"y_label": "v",
},
id="multiple_x_fields",
),
pytest.param(
{
"f": {
"metric": [
{"v": 1, "v2": 0.1, "x1": 100},
{"v": 2, "v2": 0.2, "x1": 1000},
]
},
"f2": {"metric": [{"x2": -2}, {"x2": -4}]},
},
{"y": ["v", "v2"], "x": {"f": "x1", "f2": "x2"}},
[
{
"dvc_inferred_x_value": 100,
"dvc_inferred_y_value": 1,
"v": 1,
"v2": 0.1,
"x1": 100,
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
{
"dvc_inferred_x_value": 1000,
"dvc_inferred_y_value": 2,
"v": 2,
"v2": 0.2,
"x1": 1000,
REVISION: "r",
FILENAME: "f",
FIELD: "v",
},
{
"dvc_inferred_x_value": -2,
"dvc_inferred_y_value": 0.1,
"v": 1,
"v2": 0.1,
"x1": 100,
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
{
"dvc_inferred_x_value": -4,
"dvc_inferred_y_value": 0.2,
"v": 2,
"v2": 0.2,
"x1": 1000,
REVISION: "r",
FILENAME: "f",
FIELD: "v2",
},
],
{
"anchors_y_definitions": [
{FILENAME: "f", FIELD: "v"},
{FILENAME: "f", FIELD: "v2"},
],
"x": "dvc_inferred_x_value",
"y": "dvc_inferred_y_value",
"x_label": "x",
"y_label": "y",
},
id="y_list_x_dict",
),
],
)
def test_convert(