From 4e88a665a1ddc1cd3f09d60b245c18018a51af1e Mon Sep 17 00:00:00 2001 From: gayanW Date: Mon, 8 Sep 2025 16:15:45 +0900 Subject: [PATCH 1/4] Extend --link flag to support explicit kinds --- launchable/commands/helper.py | 2 + launchable/commands/record/build.py | 11 +-- launchable/commands/record/session.py | 30 +++----- launchable/commands/record/tests.py | 5 ++ launchable/utils/link.py | 83 +++++++++++++++++++- tests/commands/record/test_build.py | 47 ++++++++++++ tests/commands/record/test_session.py | 84 +++++++++++++++++++++ tests/commands/record/test_tests.py | 104 ++++++++++++++++++++++++++ tests/utils/test_link.py | 54 ++++++++++++- 9 files changed, 390 insertions(+), 30 deletions(-) diff --git a/launchable/commands/helper.py b/launchable/commands/helper.py index ebd83e887..6606d518c 100644 --- a/launchable/commands/helper.py +++ b/launchable/commands/helper.py @@ -124,6 +124,8 @@ def find_or_create_session( session_id = read_session(saved_build_name) if session_id: _check_observation_mode_status(session_id, is_observation, tracking_client=tracking_client, app=context.obj) + if links: + click.echo(click.style("WARNING: --link option is ignored since session already exists."), err=True) return session_id context.invoke( diff --git a/launchable/commands/record/build.py b/launchable/commands/record/build.py index 321aceeff..e5dd3aa13 100644 --- a/launchable/commands/record/build.py +++ b/launchable/commands/record/build.py @@ -7,7 +7,7 @@ import click from tabulate import tabulate -from launchable.utils.link import CIRCLECI_KEY, GITHUB_ACTIONS_KEY, JENKINS_URL_KEY, LinkKind, capture_link +from launchable.utils.link import CIRCLECI_KEY, GITHUB_ACTIONS_KEY, JENKINS_URL_KEY, capture_links from launchable.utils.tracking import Tracking, TrackingClient from ...utils import subprocess @@ -316,14 +316,7 @@ def synthesize_workspaces() -> List[Workspace]: def send(ws: List[Workspace]) -> Optional[str]: # figure out all the CI links to capture def compute_links(): - _links = capture_link(os.environ) - for k, v in links: - _links.append({ - "title": k, - "url": v, - "kind": LinkKind.CUSTOM_LINK.name, - }) - return _links + return capture_links(links, os.environ) try: payload = { diff --git a/launchable/commands/record/session.py b/launchable/commands/record/session.py index b5376e1a0..d751baa9d 100644 --- a/launchable/commands/record/session.py +++ b/launchable/commands/record/session.py @@ -8,7 +8,7 @@ import click from launchable.utils.click import DATETIME_WITH_TZ, validate_past_datetime -from launchable.utils.link import LinkKind, capture_link +from launchable.utils.link import capture_links from launchable.utils.tracking import Tracking, TrackingClient from ...utils.click import KEY_VALUE @@ -181,25 +181,17 @@ def session( flavor_dict = dict(flavor) - payload = { - "flavors": flavor_dict, - "isObservation": is_observation, - "noBuild": is_no_build, - "lineage": lineage, - "testSuite": test_suite, - "timestamp": timestamp.isoformat() if timestamp else None, - } - - _links = capture_link(os.environ) - for link in links: - _links.append({ - "title": link[0], - "url": link[1], - "kind": LinkKind.CUSTOM_LINK.name, - }) - payload["links"] = _links - try: + payload = { + "flavors": flavor_dict, + "isObservation": is_observation, + "noBuild": is_no_build, + "lineage": lineage, + "testSuite": test_suite, + "timestamp": timestamp.isoformat() if timestamp else None, + "links": capture_links(links, os.environ), + } + sub_path = "builds/{}/test_sessions".format(build_name) res = client.request("post", sub_path, payload=payload) diff --git a/launchable/commands/record/tests.py b/launchable/commands/record/tests.py index 64415692a..f86ba9d03 100644 --- a/launchable/commands/record/tests.py +++ b/launchable/commands/record/tests.py @@ -229,6 +229,11 @@ def tests( is_no_build = False + if session and links: + warn_and_exit_if_fail_fast_mode( + "WARNING: `--link` and `--session` are set together.\n--link option can't be used with existing sessions." + ) + try: if is_no_build: session_id = "builds/{}/test_sessions/{}".format(NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID) diff --git a/launchable/utils/link.py b/launchable/utils/link.py index 093221551..9753e0d5c 100644 --- a/launchable/utils/link.py +++ b/launchable/utils/link.py @@ -1,5 +1,8 @@ +import re from enum import Enum -from typing import Dict, List, Mapping +from typing import Dict, List, Mapping, Sequence, Tuple + +import click JENKINS_URL_KEY = 'JENKINS_URL' JENKINS_BUILD_URL_KEY = 'BUILD_URL' @@ -18,6 +21,8 @@ CIRCLECI_BUILD_NUM_KEY = 'CIRCLE_BUILD_NUM' CIRCLECI_JOB_KEY = 'CIRCLE_JOB' +GITHUB_PR_REGEX = re.compile(r"^https://github\.com/[^/]+/[^/]+/pull/\d+$") + class LinkKind(Enum): @@ -71,3 +76,79 @@ def capture_link(env: Mapping[str, str]) -> List[Dict[str, str]]: }) return links + + +def capture_links_from_options(link_options: Sequence[Tuple[str, str]]) -> List[Dict[str, str]]: + """ + Validate user-provided link options, inferring the kind when not explicitly specified. + + Each link option is expected in the format "kind|title=url" or "title=url". + If the kind is not provided, it infers the kind based on the URL pattern. + + Returns: + A list of dictionaries, where each dictionary contains the validated title, URL, and kind for each link. + + Raises: + click.UsageError: If an invalid kind is provided or URL doesn't match with the specified kind. + """ + links = [] + for k, url in link_options: + # if k,v in format "kind|title=url" + if '|' in k: + kind, title = k.split('|', 1) + if kind not in valid_kinds(): + msg = ("Invalid kind '{}' passed to --link option.\n" + "Supported kinds are: {}".format(kind, valid_kinds())) + raise click.UsageError(click.style(msg, fg="red")) + + if not url_matches_kind(url, kind): + msg = ("Invalid url '{}' passed to --link option.\n" + "URL doesn't match with the specified kind: {}".format(url, kind)) + raise click.UsageError(click.style(msg, fg="red")) + + # if k,v in format "title=url" + else: + kind = infer_kind(url) + title = k + + links.append({ + "title": title, + "url": url, + "kind": kind, + }) + + return links + + +def capture_links(link_options: Sequence[Tuple[str, str]], env: Mapping[str, str]) -> List[Dict[str, str]]: + + links = capture_links_from_options(link_options) + + env_links = capture_link(env) + for env_link in env_links: + if not has_kind(links, env_link['kind']): + links.append(env_link) + + return links + + +def infer_kind(url: str) -> str: + if GITHUB_PR_REGEX.match(url): + return LinkKind.GITHUB_PULL_REQUEST.name + + return LinkKind.CUSTOM_LINK.name + + +def has_kind(input_links: List[Dict[str, str]], kind: str) -> bool: + return any(link for link in input_links if link['kind'] == kind) + + +def valid_kinds() -> List[str]: + return [kind.name for kind in LinkKind if kind != LinkKind.LINK_KIND_UNSPECIFIED] + + +def url_matches_kind(url: str, kind: str) -> bool: + if kind == LinkKind.GITHUB_PULL_REQUEST.name: + return bool(GITHUB_PR_REGEX.match(url)) + + return True diff --git a/tests/commands/record/test_build.py b/tests/commands/record/test_build.py index 5d6940867..6f6591146 100644 --- a/tests/commands/record/test_build.py +++ b/tests/commands/record/test_build.py @@ -308,3 +308,50 @@ def test_with_timestamp(self, mock_check_output): }, payload) self.assertEqual(read_build(), self.build_name) + + @responses.activate + @mock.patch.dict(os.environ, {"LAUNCHABLE_TOKEN": CliTestCase.launchable_token}) + def test_build_with_links(self): + # Invalid kind + result = self.cli( + "record", + "build", + "--no-commit-collection", + "--link", + "UNKNOWN_KIND|PR=https://github.com/launchableinc/cli/pull/1", + "--name", + self.build_name) + self.assertIn("Invalid kind 'UNKNOWN_KIND' passed to --link option", result.output) + + # Invalid URL + result = self.cli( + "record", + "build", + "--no-commit-collection", + "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/1/files", + "--name", + self.build_name) + self.assertIn("Invalid url 'https://github.com/launchableinc/cli/pull/1/files' passed to --link option", result.output) + + # Infer kind + result = self.cli( + "record", + "build", + "--no-commit-collection", + "--link", + "PR=https://github.com/launchableinc/cli/pull/1", + "--name", + self.build_name) + self.assert_success(result) + + # Explicit kind + result = self.cli( + "record", + "build", + "--no-commit-collection", + "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/1", + "--name", + self.build_name) + self.assert_success(result) diff --git a/tests/commands/record/test_session.py b/tests/commands/record/test_session.py index ddac44e15..b3206b802 100644 --- a/tests/commands/record/test_session.py +++ b/tests/commands/record/test_session.py @@ -5,6 +5,7 @@ import responses # type: ignore from launchable.utils.http_client import get_base_url +from launchable.utils.link import LinkKind from tests.cli_test_case import CliTestCase @@ -188,3 +189,86 @@ def test_run_session_with_timestamp(self): "testSuite": None, "timestamp": "2023-10-01T12:00:00+00:00", }, payload) + + @responses.activate + @mock.patch.dict(os.environ, { + "LAUNCHABLE_TOKEN": CliTestCase.launchable_token, + 'LANG': 'C.UTF-8', + "GITHUB_PULL_REQUEST_URL": "https://github.com/launchableinc/cli/pull/1", + }, clear=True) + def test_run_session_with_links(self): + # Endpoint to assert + endpoint = "{}/intake/organizations/{}/workspaces/{}/builds/{}/test_sessions".format( + get_base_url(), + self.organization, + self.workspace, + self.build_name) + + # Capture from environment + result = self.cli("record", "session", "--build", self.build_name) + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 0).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "", + "url": "https://github.com/launchableinc/cli/pull/1", + }], payload["links"]) + + # Priority check + result = self.cli("record", "session", "--build", self.build_name, "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2") + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 1).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/2", + }], payload["links"]) + + # Infer kind + result = self.cli("record", "session", "--build", self.build_name, "--link", + "PR=https://github.com/launchableinc/cli/pull/2") + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 2).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/2", + }], payload["links"]) + + # Explicit kind + result = self.cli("record", "session", "--build", self.build_name, "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2") + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 3).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/2", + }], payload["links"]) + + # Multiple kinds + result = self.cli("record", "session", "--build", self.build_name, "--link", + "GITHUB_ACTIONS|=https://github.com/launchableinc/mothership/actions/runs/3747451612") + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 4).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_ACTIONS.name, + "title": "", + "url": "https://github.com/launchableinc/mothership/actions/runs/3747451612", + }, + { + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "", + "url": "https://github.com/launchableinc/cli/pull/1", + }], payload["links"]) + + # Invalid kind + result = self.cli("record", "session", "--build", self.build_name, "--link", + "UNKNOWN_KIND|PR=https://github.com/launchableinc/cli/pull/2") + self.assertIn("Invalid kind 'UNKNOWN_KIND' passed to --link option", result.output) + + # Invalid URL + result = self.cli("record", "session", "--build", self.build_name, "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2/files") + self.assertIn("Invalid url 'https://github.com/launchableinc/cli/pull/2/files' passed to --link option", result.output) diff --git a/tests/commands/record/test_tests.py b/tests/commands/record/test_tests.py index 1e8c122e0..57aa72e40 100644 --- a/tests/commands/record/test_tests.py +++ b/tests/commands/record/test_tests.py @@ -9,6 +9,7 @@ from launchable.commands.record.tests import INVALID_TIMESTAMP, parse_launchable_timeformat from launchable.utils.http_client import get_base_url +from launchable.utils.link import LinkKind from launchable.utils.no_build import NO_BUILD_BUILD_NAME, NO_BUILD_TEST_SESSION_ID from launchable.utils.session import write_build, write_session from tests.cli_test_case import CliTestCase @@ -107,3 +108,106 @@ def test_when_total_test_duration_zero(self): self.assert_success(result) self.assertIn("Total test duration is 0.", result.output) + + @responses.activate + @mock.patch.dict(os.environ, { + "LAUNCHABLE_TOKEN": CliTestCase.launchable_token, + "GITHUB_PULL_REQUEST_URL": "https://github.com/launchableinc/cli/pull/1", + }) + def test_with_links(self): + # Endpoint to assert + endpoint = "{}/intake/organizations/{}/workspaces/{}/builds/{}/test_sessions".format( + get_base_url(), + self.organization, + self.workspace, + self.build_name) + + # Capture from environment + write_build(self.build_name) + result = self.cli("record", "tests", "--build", self.build_name, 'maven', str(self.report_files_dir) + "**/reports/") + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 0).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "", + "url": "https://github.com/launchableinc/cli/pull/1", + }], payload["links"]) + + # Priority check + write_build(self.build_name) + result = self.cli("record", "tests", "--build", self.build_name, "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2", 'maven', + str(self.report_files_dir) + "**/reports/") + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 1).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/2", + }], payload["links"]) + + # Infer kind + write_build(self.build_name) + result = self.cli("record", + "tests", + "--build", + self.build_name, + "--link", + "PR=https://github.com/launchableinc/cli/pull/2", + 'maven', + str(self.report_files_dir) + "**/reports/") + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 2).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/2", + }], payload["links"]) + + # Explicit kind + write_build(self.build_name) + result = self.cli("record", "tests", "--build", self.build_name, "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2", 'maven', + str(self.report_files_dir) + "**/reports/") + self.assert_success(result) + payload = json.loads(self.find_request(endpoint, 3).request.body.decode()) + self.assertEqual([{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/2", + }], payload["links"]) + + # Invalid kind + write_build(self.build_name) + result = self.cli("record", + "tests", + "--build", + self.build_name, + "--link", + "UNKNOWN_KIND|PR=https://github.com/launchableinc/cli/pull/2", + 'maven', + str(self.report_files_dir) + "**/reports/") + self.assertIn("Invalid kind 'UNKNOWN_KIND' passed to --link option", result.output) + + # Invalid URL + write_build(self.build_name) + result = self.cli("record", "tests", "--build", self.build_name, "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2/files", 'maven', + str(self.report_files_dir) + "**/reports/") + self.assertIn("Invalid url 'https://github.com/launchableinc/cli/pull/2/files' passed to --link option", result.output) + + # With --session flag + write_session(self.build_name, self.session) + result = self.cli("record", "tests", "--session", self.session, "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2/files", 'maven', + str(self.report_files_dir) + "**/reports/") + self.assertIn("WARNING: `--link` and `--session` are set together", result.output) + self.assert_success(result) + + # Existing session + write_session(self.build_name, self.session) + result = self.cli("record", "tests", "--link", + "GITHUB_PULL_REQUEST|PR=https://github.com/launchableinc/cli/pull/2/files", 'maven', + str(self.report_files_dir) + "**/reports/") + self.assertIn("WARNING: --link option is ignored since session already exists", result.output) + self.assert_success(result) diff --git a/tests/utils/test_link.py b/tests/utils/test_link.py index 3ee302b1c..b7527bb67 100644 --- a/tests/utils/test_link.py +++ b/tests/utils/test_link.py @@ -1,6 +1,8 @@ from unittest import TestCase -from launchable.utils.link import LinkKind, capture_link +import click + +from launchable.utils.link import LinkKind, capture_link, capture_links, capture_links_from_options class LinkTest(TestCase): @@ -44,3 +46,53 @@ def test_circleci(self): "title": "job (234)", "url": "https://circleci.com/build/123", }]) + + def test_capture_links_from_options(self): + # Invalid kind + link_options = [("INVALID_KIND|PR", "https://github.com/launchableinc/cli/pull/1")] + with self.assertRaises(click.UsageError): + capture_links_from_options(link_options) + + # Invalid URL + link_options = [("GITHUB_PULL_REQUEST|PR", "https://github.com/launchableinc/cli/pull/1/files")] + with self.assertRaises(click.UsageError): + capture_links_from_options(link_options) + + # Infer kind + link_options = [("PR", "https://github.com/launchableinc/cli/pull/1")] + self.assertEqual(capture_links_from_options(link_options), [{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/1", + }]) + + # Explicit kind + link_options = [("GITHUB_PULL_REQUEST|PR", "https://github.com/launchableinc/cli/pull/1")] + self.assertEqual(capture_links_from_options(link_options), [{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/1", + }]) + + def test_capture_links(self): + # Capture from environment + envs = { + "GITHUB_PULL_REQUEST_URL": "https://github.com/launchableinc/cli/pull/1" + } + link_options = [] + self.assertEqual(capture_links(link_options, envs), [{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "", + "url": "https://github.com/launchableinc/cli/pull/1", + }]) + + # Priority check + envs = { + "GITHUB_PULL_REQUEST_URL": "https://github.com/launchableinc/cli/pull/1" + } + link_options = [("GITHUB_PULL_REQUEST|PR", "https://github.com/launchableinc/cli/pull/2")] + self.assertEqual(capture_links(link_options, envs), [{ + "kind": LinkKind.GITHUB_PULL_REQUEST.name, + "title": "PR", + "url": "https://github.com/launchableinc/cli/pull/2", + }]) From d6314f1ada975079f23eda69cb4eeee6c0721b33 Mon Sep 17 00:00:00 2001 From: gayanW Date: Mon, 8 Sep 2025 17:38:57 +0900 Subject: [PATCH 2/4] Refactor tests.commands.record.test_tests.TestsTest.test_with_links --- tests/commands/record/test_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/commands/record/test_tests.py b/tests/commands/record/test_tests.py index 57aa72e40..10125e119 100644 --- a/tests/commands/record/test_tests.py +++ b/tests/commands/record/test_tests.py @@ -113,7 +113,7 @@ def test_when_total_test_duration_zero(self): @mock.patch.dict(os.environ, { "LAUNCHABLE_TOKEN": CliTestCase.launchable_token, "GITHUB_PULL_REQUEST_URL": "https://github.com/launchableinc/cli/pull/1", - }) + }, clear=True) def test_with_links(self): # Endpoint to assert endpoint = "{}/intake/organizations/{}/workspaces/{}/builds/{}/test_sessions".format( From b55e8ed044f437399b9a2f8c5f1485f4e136164b Mon Sep 17 00:00:00 2001 From: gayanW Date: Wed, 10 Sep 2025 13:52:48 +0900 Subject: [PATCH 3/4] Prefix _ to private method names in utils/link.py --- launchable/utils/link.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/launchable/utils/link.py b/launchable/utils/link.py index 9753e0d5c..fb631242c 100644 --- a/launchable/utils/link.py +++ b/launchable/utils/link.py @@ -96,19 +96,19 @@ def capture_links_from_options(link_options: Sequence[Tuple[str, str]]) -> List[ # if k,v in format "kind|title=url" if '|' in k: kind, title = k.split('|', 1) - if kind not in valid_kinds(): + if kind not in _valid_kinds(): msg = ("Invalid kind '{}' passed to --link option.\n" - "Supported kinds are: {}".format(kind, valid_kinds())) + "Supported kinds are: {}".format(kind, _valid_kinds())) raise click.UsageError(click.style(msg, fg="red")) - if not url_matches_kind(url, kind): + if not _url_matches_kind(url, kind): msg = ("Invalid url '{}' passed to --link option.\n" "URL doesn't match with the specified kind: {}".format(url, kind)) raise click.UsageError(click.style(msg, fg="red")) # if k,v in format "title=url" else: - kind = infer_kind(url) + kind = _infer_kind(url) title = k links.append({ @@ -126,28 +126,28 @@ def capture_links(link_options: Sequence[Tuple[str, str]], env: Mapping[str, str env_links = capture_link(env) for env_link in env_links: - if not has_kind(links, env_link['kind']): + if not _has_kind(links, env_link['kind']): links.append(env_link) return links -def infer_kind(url: str) -> str: +def _infer_kind(url: str) -> str: if GITHUB_PR_REGEX.match(url): return LinkKind.GITHUB_PULL_REQUEST.name return LinkKind.CUSTOM_LINK.name -def has_kind(input_links: List[Dict[str, str]], kind: str) -> bool: +def _has_kind(input_links: List[Dict[str, str]], kind: str) -> bool: return any(link for link in input_links if link['kind'] == kind) -def valid_kinds() -> List[str]: +def _valid_kinds() -> List[str]: return [kind.name for kind in LinkKind if kind != LinkKind.LINK_KIND_UNSPECIFIED] -def url_matches_kind(url: str, kind: str) -> bool: +def _url_matches_kind(url: str, kind: str) -> bool: if kind == LinkKind.GITHUB_PULL_REQUEST.name: return bool(GITHUB_PR_REGEX.match(url)) From 56ee7f5b2fbe58282915ae03eda0ffb04c0de98d Mon Sep 17 00:00:00 2001 From: gayanW Date: Wed, 10 Sep 2025 14:37:42 +0900 Subject: [PATCH 4/4] Strip values pass to --link option --- launchable/utils/link.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/launchable/utils/link.py b/launchable/utils/link.py index fb631242c..a017a1513 100644 --- a/launchable/utils/link.py +++ b/launchable/utils/link.py @@ -93,9 +93,11 @@ def capture_links_from_options(link_options: Sequence[Tuple[str, str]]) -> List[ """ links = [] for k, url in link_options: + url = url.strip() + # if k,v in format "kind|title=url" if '|' in k: - kind, title = k.split('|', 1) + kind, title = (part.strip() for part in k.split('|', 1)) if kind not in _valid_kinds(): msg = ("Invalid kind '{}' passed to --link option.\n" "Supported kinds are: {}".format(kind, _valid_kinds())) @@ -109,7 +111,7 @@ def capture_links_from_options(link_options: Sequence[Tuple[str, str]]) -> List[ # if k,v in format "title=url" else: kind = _infer_kind(url) - title = k + title = k.strip() links.append({ "title": title,