-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support experiment name and description in uploader #3277
Conversation
tensorboard/uploader/uploader.py
Outdated
""" | ||
logger.info("Modifying experiment %r", experiment_id) | ||
if name is not None: | ||
logger.info("Setting exp %r name to ", experiment_id, name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too many arguments for format string; you need another %r
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks
tensorboard/uploader/uploader.py
Outdated
request = write_service_pb2.UpdateExperimentRequest( | ||
experiment=experiment, experiment_mask=experiment_mask | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this deep-copies the protos (automatically, to avoid shared
mutable state). To avoid this, we typically populate fields on the
request directly:
request = write_service_pb2.UpdateExperimentRequest()
request.experiment.experiment_id = experiment_id
if name is not None:
logger.info(...)
request.experiment.name = name
request.experiment_mask.name = True
if description is not None:
logger.info(...)
request.experiment.description = description
request.experiment_mask.description = True
(assuming that request.experiment_mask.SetInParent()
does not change
the semantics; add that if it would).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know re copy semantics. Easy to forget.
if name and len(name) > _EXPERIMENT_NAME_MAX_CHARS: | ||
raise ValueError( | ||
"Experiment name is too long. Limit is " | ||
f"{_EXPERIMENT_NAME_MAX_CHARS} characters.\n" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a PY2AND3
module, but f-strings are a PY3-only feature, and
don’t provide enough value to change this whole target to PY3 (which is
also not yet possible due to Google-internal infrastructure; I’m
actively working on this). Please revert to normal format strings (of
either style), lest this break at sync time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, thanks.
print("Modified experiment %s." % experiment_id) | ||
if self.name is not None: | ||
print(f"Set name to {repr(self.name)}") | ||
if self.description is not None: | ||
print(f"Set description to {repr(self.description)}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UX: It’s fine to logging.info
here for debugging, but it’s good form
for commands like this to print nothing on success (consider Unix
chmod
/mv
/sed
, VCS hg reword
/git amend
, etc.). Using stdout for
“everything is OK” messages is a very Windows thing to do. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI : the delete command above prints such a message. Should we remove it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, I'm leaving them as logging.info, as you suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, delete
does print. IMHO, this is a bit more justified because
permanently deleting an experiment is a more dangerous operation than
updating metadata, so the deletion confirmation is something that you
might want to show up in an audit log. For comparison, git rm
prints
what it deleted, but git add
does not print what it added.
If we want to remove the printing on delete
in a separate PR, that’d
be fine with me. No strong opinion.
@@ -417,7 +527,7 @@ def execute(self, server_info, channel): | |||
("Tags", str(experiment.num_tags)), | |||
] | |||
for (name, value) in data: | |||
print("\t%s %s" % (name.ljust(10), value)) | |||
print("\t%s %s" % (name.ljust(11), value)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Can we use 12 rather than 11 to keep aligned at an even column
boundary? Makes it a bit easier for folks to work with this text in a
normal editor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, done, but can you explain a little more why even column boundaries are better for normal editors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing huge; just that pressing <Tab>
generally indents to the next
tab stop (if you have expandtab
or equivalent set), and 12 is much
more likely to be that tab stop than 11 is. Hence “nit”. :-)
) | ||
try: | ||
grpc_util.call_with_retries(writer_client.UpdateExperiment, request) | ||
except grpc.RpcError as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won’t we need to handle INVALID_ARGUMENT
in the case where the server
raises a validation error? e.g., if we change the size limits on the
server side and people run update-metadata
with an older client?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I wasn't clear exactly how we want to handle this. I suppose we could pass along the details if they are populated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, passing along the details sounds good to me.
eid_name = uploader_name.create_experiment() | ||
self.assertEqual(eid_name, "123") | ||
eid_description = uploader_description.create_experiment() | ||
self.assertEqual(eid_description, "123") | ||
eid_both = uploader_both.create_experiment() | ||
self.assertEqual(eid_both, "123") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to test that these actually send the proper requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Broke this into 3 tests.
tensorboard/uploader/uploader.py
Outdated
ExperimentNotFoundError: If no such experiment exists. | ||
PermissionDeniedError: If the user is not authorized to modify this | ||
experiment. | ||
RuntimeError: On unexpected failure. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this superfluous? The method doesn’t appear to raise RuntimeError
directly, and “we might raise an arbitrary exception due to a
programming error” isn’t really part of the public API (cf. style guide
on “Raises”).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. This was cloned to be consistent with delete_experiment
below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. This was here by copying from above.
@@ -75,6 +75,25 @@ def test_create_experiment(self): | |||
eid = uploader.create_experiment() | |||
self.assertEqual(eid, "123") | |||
|
|||
def test_create_experiment_with_name_and_description(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add tests for update_metadata
, including requests sent and
failure handling.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See UpdateExperimentMetadataTest below. Is that sufficient?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved modulo fixing the error handling (.details()
); thanks!
) | ||
try: | ||
grpc_util.call_with_retries(writer_client.UpdateExperiment, request) | ||
except grpc.RpcError as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, passing along the details sounds good to me.
tensorboard/uploader/uploader.py
Outdated
details = "" | ||
if hasattr(e, "details"): | ||
details = e.details | ||
raise InvalidArgumentError(details) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few issues here:
-
On gRPC errors,
code
anddetails
are callables, not the raw
values (notee.code()
above). Thus, the actual exception message
is something like<function grpc_error.<locals>.<lambda> at 0x7f18ea696f80>
which is not what was wanted. Unfortunately, this case is not
tested, so the failure is not caught. -
Why the conditional? I was under the impression that
details()
always exists on anRpcError
value. If we do need the conditional,
it should just begetattr(e, "details", lambda: "")()
. The
hasattr
builtin is rarely what you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done. Added test for invalidArgument condition.
class InvalidArgumentError(RuntimeError): | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK: We do have tensorboard.errors.InvalidArgumentError
already, but
it’s reasonable to want to keep these local, and it’s not a problem to
change later if we change our minds.
if flags.experiment_id: | ||
if flags.name is not None or flags.description is not None: | ||
return _UpdateMetadataIntent( | ||
flags.experiment_id, | ||
name=flags.name, | ||
description=flags.description, | ||
) | ||
else: | ||
raise base_plugin.FlagsError( | ||
"Must specify either `--name` or `--description`." | ||
) | ||
else: | ||
raise base_plugin.FlagsError( | ||
"Must specify experiment to modify via `--experiment_id`." | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Can we use early returns here to avoid pushing the code more and
more to the right?
if not flags.experiment_id:
raise base_plugin.FlagsError(
"Must specify experiment to modify via `--experiment_id`."
)
if flags.name is None and flags.description is None:
raise base_plugin.FlagsError(
"Must specify either `--name` or `--description`."
)
return _UpdateMetadataIntent(
flags.experiment_id,
name=flags.name,
description=flags.description,
)
mock_client = _create_mock_client() | ||
new_description = """ | ||
**description**" | ||
may have "strange" unicode chars 🌴 \/<> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The \/
is a syntax warning in Python 3.8+ and will be a syntax error
later (invalid escape sequence). Use \\/
or raw string literal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
if name is not None: | ||
logger.info("Setting exp %r name to %r", experiment_id, name) | ||
request.experiment.name = name | ||
request.experiment_mask.name = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly in this PR, but it’s confusing that this field is not called
update_mask
(per AIP 134); if it’s possible to change this before
stable launch, that would be great.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not doing this in this PR. Let's discuss elsewhere if we can fit this in before stable launch.
I’ve changed the title of this pull request to match our conventions Friendly optional suggestion: You may find it convenient to enable your |
* Adds name and description to the output of "list" subcommand * Adds name and description as upload options to the uploader * add subcommand for update-metadata to change name and description * black * adds additional testing. Fixes a logging error * Adds additional testing. Changes print output to log.info * black * stray keystroke * black * Adds test for INVALID_ARGUMENT. Addresses more reviewer critiques * black & stray edit * evenblacker
* Adds name and description to the output of "list" subcommand * Adds name and description as upload options to the uploader * add subcommand for update-metadata to change name and description * black * adds additional testing. Fixes a logging error * Adds additional testing. Changes print output to log.info * black * stray keystroke * black * Adds test for INVALID_ARGUMENT. Addresses more reviewer critiques * black & stray edit * evenblacker
Motivation for features / changes
Adds the ability to list, upload, and modify experiments with names & descriptions.
Technical description of changes
Adds user intent & checking for
Screenshots of UI changes
bazel run //tensorboard -- dev upload --logdir /tmp/scalars_demo
bazel run //tensorboard -- dev upload --logdir /tmp/scalars_demo --name="my first name"
bazel run //tensorboard -- dev upload --logdir /tmp/scalars_demo --name="my first name" --description="**********************************************************************************************************************************************************************************************************************************************"
bazel run //tensorboard -- dev update-metadata --experiment_id=F0EqjQjYTOCk4AdY5oskbQ --description 'a description 💁👌🎍😍'