From 8b61db82b5f955075009b99c051bc69db82a7c54 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Fri, 1 Dec 2023 20:22:50 -0500 Subject: [PATCH 01/35] fix: make repeated text keep the spinner Fixes #138 --- craft_cli/printer.py | 56 +++++++++++------- examples.py | 27 +++++++++ tests/unit/test_printer.py | 116 +++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 20 deletions(-) diff --git a/craft_cli/printer.py b/craft_cli/printer.py index 2a3b267..c18e4c4 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -56,7 +56,7 @@ class _MessageInfo: bar_total: int | float | None = None use_timestamp: bool = False end_line: bool = False - created_at: datetime = field(default_factory=datetime.now) + created_at: datetime = field(default_factory=datetime.now, compare=False) terminal_prefix: str = "" @@ -71,6 +71,27 @@ def _get_terminal_width() -> int: return shutil.get_terminal_size().columns +def _format_term_line(prefix: str, text: str, spintext: str, *, ephemeral: bool) -> str: + """Format a line to print to the terminal.""" + # fill with spaces until the very end, on one hand to clear a possible previous message, + # but also to always have the cursor at the very end + width = _get_terminal_width() + usable = width - len(spintext) - 1 # the 1 is the cursor itself + if len(text) > usable: + if ephemeral: + text = text[: usable - 1] + "…" + elif spintext: + # we need to rewrite the message with the spintext, use only the last line for + # multiline messages, and ensure (again) that the last real line fits + remaining_for_last_line = len(text) % width + text = text[-remaining_for_last_line:] + if len(text) > usable: + text = text[: usable - 1] + "…" + cleaner = " " * (usable - len(text) % width) + + return prefix + text + spintext + cleaner + + class _Spinner(threading.Thread): """A supervisor thread that will repeat long-standing messages with a spinner besides it. @@ -105,6 +126,9 @@ def __init__(self, printer: Printer) -> None: # a lock to wait the spinner to stop spinning self.lock = threading.Lock() + # Keep the message under supervision available for examination. + self._under_supervision: _MessageInfo | None = None + def run(self) -> None: prv_msg = None t_init = time.time() @@ -136,6 +160,11 @@ def run(self) -> None: def supervise(self, message: _MessageInfo | None) -> None: """Supervise a message to spin it if it remains too long.""" + # Don't bother the spinner if we're repeating the same message + if message == self._under_supervision: + return + + self._under_supervision = message self.queue.put(message) # (maybe) wait for the spinner to exit spinning state (which does some cleaning) self.lock.acquire() @@ -200,7 +229,6 @@ def _get_prefixed_message_text(self, message: _MessageInfo) -> str: def _write_line_terminal(self, message: _MessageInfo, *, spintext: str = "") -> None: """Write a simple line message to the screen.""" # prepare the text with (maybe) the timestamp - text = self._get_prefixed_message_text(message) if message.use_timestamp: @@ -226,24 +254,12 @@ def _write_line_terminal(self, message: _MessageInfo, *, spintext: str = "") -> maybe_cr = "" print(flush=True, file=self.prv_msg.stream) - # fill with spaces until the very end, on one hand to clear a possible previous message, - # but also to always have the cursor at the very end - width = _get_terminal_width() - usable = width - len(spintext) - 1 # the 1 is the cursor itself - if len(text) > usable: - if message.ephemeral: - text = text[: usable - 1] + "…" - elif spintext: - # we need to rewrite the message with the spintext, use only the last line for - # multiline messages, and ensure (again) that the last real line fits - remaining_for_last_line = len(text) % width - text = text[-remaining_for_last_line:] - if len(text) > usable: - text = text[: usable - 1] + "…" - cleaner = " " * (usable - len(text) % width) - - line = maybe_cr + text + spintext + cleaner - print(line, end="", flush=True, file=message.stream) + # We don't need to rewrite the same ephemeral message repeatedly. + should_overwrite = spintext or message.end_line or not message.ephemeral + if should_overwrite or message != self.prv_msg: + line = _format_term_line(maybe_cr, text, spintext, ephemeral=message.ephemeral) + print(line, end="", flush=True, file=message.stream) + if message.end_line: # finish the just shown line, as we need a clean terminal for some external thing print(flush=True, file=message.stream) diff --git a/examples.py b/examples.py index ebd9570..c842a1a 100755 --- a/examples.py +++ b/examples.py @@ -486,6 +486,33 @@ def _call_lib(logger, index): time.sleep(2) +def example_30(): + """Message spamming, noting the different spinner behaviour""" + emit.progress( + "Message spamming example. The same message will be spammed for 10s, but " + "it will appear as one message with a spinner.", + permanent=True, + ) + end_time = time.monotonic() + 10 + while time.monotonic() < end_time: + emit.progress("SPAM SPAM SPAM SPAM") + time.sleep(0.001) + emit.progress( + "Now two separate messages will be spammed and no spinner appear.", permanent=True + ) + end_time = time.monotonic() + 10 + while time.monotonic() < end_time: + emit.progress("SPAM SPAM SPAM SPAM") + time.sleep(0.01) + emit.progress("SPAM SPAM SPAM baked beans") + time.sleep(0.01) + emit.progress("And back to the first message!", permanent=True) + end_time = time.monotonic() + 10 + while time.monotonic() < end_time: + emit.progress("SPAM SPAM SPAM SPAM") + time.sleep(0.001) + + # -- end of test cases if len(sys.argv) < 2: diff --git a/tests/unit/test_printer.py b/tests/unit/test_printer.py index 204d719..0368d1f 100644 --- a/tests/unit/test_printer.py +++ b/tests/unit/test_printer.py @@ -364,6 +364,82 @@ def test_writelineterminal_spintext_length_just_exceeded(capsys, monkeypatch, lo assert out == "\r0x1x2x3x4x… * 3.15s" +@pytest.mark.parametrize("test_text", ["", "Some test text."]) +def test_writelineterminal_ephemeral_spam(capsys, monkeypatch, log_filepath, test_text): + """Spam _write_line_terminal with the same message over and over.""" + monkeypatch.setattr(printermod, "_get_terminal_width", lambda: 40) + printer = Printer(log_filepath) + + for _ in range(10): + # Recreate the message here so we're checking equality, not just identity. + msg = _MessageInfo(sys.stdout, test_text, end_line=False, ephemeral=True) + printer._write_line_terminal(msg) + printer.prv_msg = msg + + assert printer.unfinished_stream == sys.stdout + + out, err = capsys.readouterr() + assert not err + + # output completes the terminal width (leaving space for the cursor), and + # without a finishing newline + # There will only be one copy of the text. + assert out == test_text[:40] + " " * (39 - len(test_text)) + + +@pytest.mark.parametrize(("ephemeral", "end_line"), [(False, False), (False, True), (True, True)]) +@pytest.mark.parametrize("text", ["", "Some test text"]) +def test_writelineterminal_rewrites_same_message( + capsys, monkeypatch, log_filepath, text, ephemeral, end_line +): + """Spam _write_line_terminal with the same message and ensure it keeps writing.""" + monkeypatch.setattr(printermod, "_get_terminal_width", lambda: 40) + printer = Printer(log_filepath) + + for _ in range(10): + message = _MessageInfo(sys.stdout, text, ephemeral=ephemeral, end_line=end_line) + printer._write_line_terminal(message) + printer.prv_msg = message + + out, err = capsys.readouterr() + assert not err + + # output completes the terminal width (leaving space for the cursor), and + # without a finishing newline + assert out.strip() == "\n".join([text + " " * (39 - len(text))] * 10).strip() + + +@pytest.mark.parametrize("ephemeral", [True, False]) +@pytest.mark.parametrize("text", ["", "Some test text"]) +@pytest.mark.parametrize( + "spintext", + [ + "!!!!!!!!!!", + "\\|/-", + "1234567890", + ], +) +def test_writelineterminal_rewrites_same_message_with_spintext( + capsys, monkeypatch, log_filepath, text, spintext, ephemeral +): + """Spam _write_line_terminal with the same message over and over.""" + monkeypatch.setattr(printermod, "_get_terminal_width", lambda: 40) + printer = Printer(log_filepath) + + for spin in spintext: + message = _MessageInfo(sys.stdout, text, ephemeral=ephemeral, end_line=False) + printer._write_line_terminal(message, spintext=spin) + printer.prv_msg = message + + out, err = capsys.readouterr() + assert not err + + # output completes the terminal width (leaving space for the cursor), and + # without a finishing newline + expected = "\r".join(text + s + " " * (39 - len(text) - len(s)) for s in spintext) + assert out.strip() == expected.strip() + + # -- tests for the writing line (captured version) function @@ -1055,6 +1131,46 @@ def test_spinner_working_simple(spinner, monkeypatch): assert spinner.printer.spinned[-1] == (msg, " ") +def test_spinner_spam(spinner, monkeypatch): + """Test that the spinner works properly when spamming the same message. + + The expected behaviour is to ignore the existence of the fresh message and just + write when the spinner needs to update. + """ + # set absurdly low times so we can have several spin texts in the test + monkeypatch.setattr(printermod, "_SPINNER_THRESHOLD", 0.001) + monkeypatch.setattr(printermod, "_SPINNER_DELAY", 0.001) + + # send a message, wait enough until we have enough spinned to test, and turn it off + msg = _MessageInfo(sys.stdout, "test msg") + for _ in range(100): + spinner.supervise(_MessageInfo(sys.stdout, "test msg")) + for _ in range(100): + if len(spinner.printer.spinned) >= 6: + break + time.sleep(0.01) + else: + pytest.fail("Waited too long for the _Spinner to generate messages") + spinner.supervise(None) + to_check = spinner.printer.spinned[:5] + + # check the initial messages complete the "spinner drawing" also showing elapsed time + spinned_messages, spinned_texts = list(zip(*to_check)) + assert all(spinned_msg == msg for spinned_msg in spinned_messages) + expected_texts = ( + r" - \(\d\.\ds\)", + r" \\ \(\d\.\ds\)", + r" | \(\d\.\ds\)", + r" / \(\d\.\ds\)", + r" - \(\d\.\ds\)", + ) + for expected, real in list(zip(expected_texts, spinned_texts)): + assert re.match(expected, real) + + # the last message should clean the spinner + assert spinner.printer.spinned[-1] == (msg, " ") + + def test_spinner_two_messages(spinner, monkeypatch): """Two consecutive messages with spinner.""" # set absurdly low times so we can have several spin texts in the test From 9e017d4c526f25ae0688e21b95ed9db694a351ca Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Thu, 18 Jan 2024 15:47:00 -0300 Subject: [PATCH 02/35] fix: handle errors when decoding bytes from subprocesses (#222) Since these bytes come from the output of unknown processes, there is no guarantee that they are valid utf-8 text. We also have no way to know what encoding they have (if any), so the best we can do is replace the invalid bytes with the usual unicode marker for that. Fixes #221 --- craft_cli/messages.py | 5 +++-- tests/unit/test_messages_stream_cm.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/craft_cli/messages.py b/craft_cli/messages.py index 75e199a..baffb02 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -239,8 +239,9 @@ def _write(self, data: bytes) -> None: useful_line = data[pointer:newline_position] pointer = newline_position + 1 - # write the useful line to intended outputs - unicode_line = useful_line.decode("utf8") + # write the useful line to intended outputs. Decode with errors="replace" + # here because we don't know where this line is coming from. + unicode_line = useful_line.decode("utf8", errors="replace") # replace tabs with a set number of spaces so that the printer # can correctly count the characters. unicode_line = unicode_line.replace("\t", " ") diff --git a/tests/unit/test_messages_stream_cm.py b/tests/unit/test_messages_stream_cm.py index b1ffcb5..dbba643 100644 --- a/tests/unit/test_messages_stream_cm.py +++ b/tests/unit/test_messages_stream_cm.py @@ -257,6 +257,24 @@ def test_pipereader_tabs(recording_printer, stream): assert msg.text == ":: 123 456" # tabs expanded into 2 spaces +@pytest.mark.parametrize( + ("invalid_text", "expected"), + [(b"\xf0\x28\x8c\xbc", "�(��"), (b"\xf0\x90\x28\xbc", "�(�"), (b"\xf0\x90\x8c\x28", "�(")], +) +def test_pipereader_invalid_utf8(recording_printer, invalid_text, expected): + """Check that bytes that aren't valid utf-8 text don't crash.""" + invalid_bytes = b"valid prefix " + invalid_text + b" valid suffix\n" + + flags = {"use_timestamp": False, "ephemeral": False, "end_line": True} + prt = _PipeReaderThread(recording_printer, sys.stdout, flags) + prt.start() + os.write(prt.write_pipe, invalid_bytes) + prt.stop() + + (msg,) = recording_printer.written_terminal_lines + assert msg.text == f":: valid prefix {expected} valid suffix" + + def test_pipereader_chunk_assembler(recording_printer, monkeypatch): """Converts ok arbitrary chunks to lines.""" monkeypatch.setattr(messages, "_PIPE_READER_CHUNK_SIZE", 5) From 5a53a011c17922701e458f6af2667ab16ce99c4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 14:26:25 -0300 Subject: [PATCH 03/35] chore(deps): update development dependencies (non-major) (#216) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 038f6b6..06f70fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,21 +43,21 @@ emitter = "craft_cli.pytest_plugin" [project.optional-dependencies] dev = [ - "coverage[toml]==7.3.2", - "pytest==7.4.3", + "coverage[toml]==7.4.1", + "pytest==7.4.4", "pytest-cov==4.1.0", "pytest-mock==3.12.0", "pytest-subprocess" ] lint = [ - "black==23.11.0", + "black==23.12.1", "codespell[toml]==2.2.6", "ruff==0.1.8", "yamllint==1.33.0" ] types = [ - "mypy[reports]==1.7.1", - "pyright==1.1.337", + "mypy[reports]==1.8.0", + "pyright==1.1.348", "types-Pygments", "types-colorama", "types-setuptools", From 0509d2060d8df6095949225e51e683ac1b9bd396 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:14:43 -0500 Subject: [PATCH 04/35] chore(deps): update github actions (major) (#213) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/docs.yaml | 4 ++-- .github/workflows/tests.yaml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index cdbc402..b770b65 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -21,7 +21,7 @@ jobs: with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install Tox @@ -31,7 +31,7 @@ jobs: - name: Build documentation run: tox run -e build-docs - name: Upload documentation - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: docs path: docs/_build/ diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 652d76b..a9a2dbe 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -17,7 +17,7 @@ jobs: with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' - name: Configure environment @@ -48,7 +48,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python versions on ${{ matrix.platform }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: | 3.8 @@ -73,7 +73,7 @@ jobs: files: coverage*.xml - name: Upload test results if: success() || failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.platform }} path: results/ From 2d805bc9535fe436f5b4b384dd39b625e56aaa23 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 09:08:50 -0300 Subject: [PATCH 05/35] chore(deps): update dependency dev/pytest to v8 (#226) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 06f70fb..8326fdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ emitter = "craft_cli.pytest_plugin" [project.optional-dependencies] dev = [ "coverage[toml]==7.4.1", - "pytest==7.4.4", + "pytest==8.0.0", "pytest-cov==4.1.0", "pytest-mock==3.12.0", "pytest-subprocess" From c40d80ecf736d3f265f3f80bda3ee6d0697c2499 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 09:16:35 -0300 Subject: [PATCH 06/35] chore(deps): update dependency lint/ruff to v0.1.14 (#223) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8326fdd..fbff90f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dev = [ lint = [ "black==23.12.1", "codespell[toml]==2.2.6", - "ruff==0.1.8", + "ruff==0.1.14", "yamllint==1.33.0" ] types = [ From cd8f394589cc7b45bfafc02af51146d07bdce2af Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 07:23:09 -0500 Subject: [PATCH 07/35] chore(deps): update dependency types/pyright to v1.1.349 (#225) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fbff90f..52a76c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ lint = [ ] types = [ "mypy[reports]==1.8.0", - "pyright==1.1.348", + "pyright==1.1.349", "types-Pygments", "types-colorama", "types-setuptools", From a395bab4b2cfc0fce6fb655f3bfd8beb75a038e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 08:44:56 -0300 Subject: [PATCH 08/35] chore(deps): update dependency setuptools to v67.8.0 (#224) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 52a76c7..c38fb28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ types = [ [build-system] requires = [ - "setuptools==67.7.2", + "setuptools==67.8.0", "setuptools_scm[toml]>=7.1" ] build-backend = "setuptools.build_meta" From b78c4ca4362ce480f9a8b2fbe98c3c3d62376ae6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:30:35 +0000 Subject: [PATCH 09/35] chore(deps): update dependency lint/ruff to v0.1.15 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c38fb28..d878dbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dev = [ lint = [ "black==23.12.1", "codespell[toml]==2.2.6", - "ruff==0.1.14", + "ruff==0.1.15", "yamllint==1.33.0" ] types = [ From bef83c67c780b7ffbeab8701ea92a61a19f794fc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:35:35 -0300 Subject: [PATCH 10/35] chore(deps): update dependency lint/black to v24 (#227) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d878dbe..04d3068 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dev = [ "pytest-subprocess" ] lint = [ - "black==23.12.1", + "black==24.1.1", "codespell[toml]==2.2.6", "ruff==0.1.15", "yamllint==1.33.0" From 99394c93564e7a34c6263369f5f3db2678f0a269 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:19:09 -0500 Subject: [PATCH 11/35] chore(deps): update dependency setuptools to v69 (#229) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 04d3068..a29acd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ types = [ [build-system] requires = [ - "setuptools==67.8.0", + "setuptools==69.0.3", "setuptools_scm[toml]>=7.1" ] build-backend = "setuptools.build_meta" From b4521174ab73e109caca6f70f7466999575b5b61 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 20:05:17 -0500 Subject: [PATCH 12/35] chore(deps): update github actions (major) (#228) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/release-drafter.yaml | 2 +- .github/workflows/tests.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml index 3ae8faa..4c6db3e 100644 --- a/.github/workflows/release-drafter.yaml +++ b/.github/workflows/release-drafter.yaml @@ -11,6 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Release Drafter - uses: release-drafter/release-drafter@v5.25.0 + uses: release-drafter/release-drafter@v6.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a9a2dbe..3432c1d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -67,7 +67,7 @@ jobs: - name: Test with tox run: tox run-parallel --parallel all --parallel-no-spinner --skip-pkg-install --result-json results/tox-${{ matrix.platform }}.json -m tests -- --no-header --quiet -rN - name: Upload code coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: directory: ./results/ files: coverage*.xml From 1aaf88836cf717a9e4d9f35a596e0f29fb4d34b9 Mon Sep 17 00:00:00 2001 From: Sergio Schvezov Date: Fri, 9 Feb 2024 15:39:06 -0300 Subject: [PATCH 13/35] ci: use GH/JIRA integration bot Signed-off-by: Sergio Schvezov --- .github/.jira_sync_config.yaml | 25 +++++++++++++++++++++++++ .github/workflows/issues.yaml | 17 ----------------- 2 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 .github/.jira_sync_config.yaml delete mode 100644 .github/workflows/issues.yaml diff --git a/.github/.jira_sync_config.yaml b/.github/.jira_sync_config.yaml new file mode 100644 index 0000000..704dad2 --- /dev/null +++ b/.github/.jira_sync_config.yaml @@ -0,0 +1,25 @@ +settings: + components: + - Craft CLI + labels: + - Bug + - Enhancement + - Spike + - Epic + # Adds a comment with the JIRA ID + add_gh_comment: true + # Reflect changes on JIRA + sync_description: true + # Comments are synced from Github to JIRA + # Nothing goes from JIRA to Github + sync_comments: true + # epic_key: "MTC-296" + jira_project_key: "CRAFT" + status_mapping: + opened: Untriaged + closed: done + label_mapping: + Enhancement: Story + Bug: Bug + Spike: Spike + Epic: Epic diff --git a/.github/workflows/issues.yaml b/.github/workflows/issues.yaml deleted file mode 100644 index d3e1264..0000000 --- a/.github/workflows/issues.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# this workflow requires to provide JIRA webhook URL via JIRA_URL GitHub Secret -# read more: https://support.atlassian.com/cloud-automation/docs/jira-automation-triggers/#Automationtriggers-Incomingwebhook -# original code source: https://github.com/beliaev-maksim/github-to-jira-automation - -name: Issues to JIRA - -on: - issues: - # available via github.event.action - types: [opened, reopened, closed] - -jobs: - update: - name: Update Issue - uses: beliaev-maksim/github-to-jira-automation/.github/workflows/issues_to_jira.yaml@master - secrets: - JIRA_URL: ${{ secrets.JIRA_URL }} From fc7221d0166a3d70967c2d5a78a73a2fc2a0c4bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 11:01:10 -0500 Subject: [PATCH 14/35] chore(deps): update development dependencies (non-major) (#234) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a29acd7..7706b52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,11 +53,11 @@ lint = [ "black==24.1.1", "codespell[toml]==2.2.6", "ruff==0.1.15", - "yamllint==1.33.0" + "yamllint==1.34.0" ] types = [ "mypy[reports]==1.8.0", - "pyright==1.1.349", + "pyright==1.1.350", "types-Pygments", "types-colorama", "types-setuptools", From 31a3c48b0a8668e5f6226b77b7086e0238c516be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:06:43 -0500 Subject: [PATCH 15/35] chore(deps): update dependency lint/black to v24.2.0 (#239) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7706b52..1e9a30e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dev = [ "pytest-subprocess" ] lint = [ - "black==24.1.1", + "black==24.2.0", "codespell[toml]==2.2.6", "ruff==0.1.15", "yamllint==1.34.0" From 675d42ab9338a922cb7272d736f664d392c2dc55 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Thu, 15 Feb 2024 12:12:25 -0500 Subject: [PATCH 16/35] style: fix pyright config (#237) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1e9a30e..320c5ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,8 +124,8 @@ exclude_also = [ pythonVersion = "3.8" pythonPlatform = "Linux" ignore = [ - "build/*", - "docs/*", + "build/**", + "docs/**", "craft_cli/_version.py", ] From 4a3ba4d77997f44b36a6c10b0a6708d03d0b01c9 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 20 Feb 2024 08:14:31 -0500 Subject: [PATCH 17/35] fix(dispatcher): explicitly disable exception chaining --- craft_cli/dispatcher.py | 12 ++++++------ pyproject.toml | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/craft_cli/dispatcher.py b/craft_cli/dispatcher.py index 7a90bbf..b38bb35 100644 --- a/craft_cli/dispatcher.py +++ b/craft_cli/dispatcher.py @@ -311,7 +311,7 @@ def _get_requested_help( # noqa: PLR0912 (too many branches) except KeyError: allowed = (repr(of.name) for of in OutputFormat) msg = f"Invalid value for --format; allowed are: {', '.join(sorted(allowed))}" - raise self._build_usage_exc(msg) # noqa: TRY200 (Use `raise from`) + raise self._build_usage_exc(msg) from None if len(filtered_params) == 1: # at this point the remaining parameter should be a command @@ -329,7 +329,7 @@ def _get_requested_help( # noqa: PLR0912 (too many branches) cmd_class = self.commands[cmdname] except KeyError: msg = f"command {cmdname!r} not found to provide help for" - raise self._build_usage_exc(msg) # noqa: TRY200 (Use `raise from`) + raise self._build_usage_exc(msg) from None # instantiate the command and fill its arguments command = cmd_class(None) @@ -402,7 +402,7 @@ def _parse_options( # noqa: PLR0912 (too many branches) global_args[arg.name] = next(sysargs_it) except StopIteration: msg = f"The {arg.name!r} option expects one argument." - raise self._build_usage_exc(msg) # noqa: TRY200 (use 'raise from') + raise self._build_usage_exc(msg) from None elif sysarg.startswith(tuple(options_with_equal)): option, value = sysarg.split("=", 1) arg = arg_per_option[option] @@ -439,10 +439,10 @@ def pre_parse_args(self, sysargs: list[str]) -> dict[str, Any]: try: verbosity_level = EmitterMode[global_args["verbosity"].upper()] except KeyError: - raise self._build_usage_exc( # noqa: TRY200 (use 'raise from') + raise self._build_usage_exc( "Bad verbosity level; valid values are " "'quiet', 'brief', 'verbose', 'debug' and 'trace'." - ) + ) from None emit.set_mode(verbosity_level) emit.trace(f"Raw pre-parsed sysargs: args={global_args} filtered={filtered_sysargs}") @@ -474,7 +474,7 @@ def pre_parse_args(self, sysargs: list[str]) -> dict[str, Any]: self._command_class = self.commands[command] except KeyError: help_text = self._build_no_command_error(command) - raise ArgumentParsingError(help_text) # noqa: TRY200 (use raise from) + raise ArgumentParsingError(help_text) from None emit.trace(f"General parsed sysargs: command={ command!r} args={cmd_args}") return global_args diff --git a/pyproject.toml b/pyproject.toml index 320c5ff..fd5dd46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dev = [ lint = [ "black==24.2.0", "codespell[toml]==2.2.6", - "ruff==0.1.15", + "ruff~=0.2.1", "yamllint==1.34.0" ] types = [ @@ -210,6 +210,7 @@ extend-select = [ "ANN0", # Type annotations for arguments other than `self` and `cls` "ANN2", # Return type annotations "B026", # Keyword arguments must come after starred arguments + "B904", # re-raising an exception should include a `from`. # flake8-bandit: security testing. https://github.com/charliermarsh/ruff#flake8-bandit-s # https://bandit.readthedocs.io/en/latest/plugins/index.html#complete-test-plugin-listing "S101", "S102", # assert or exec From 44de6d70b8ccc18cb88291175553a65db27143c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 07:16:31 -0500 Subject: [PATCH 18/35] chore(deps): update dependency setuptools to v69.1.1 (#238) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fd5dd46..e228123 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ types = [ [build-system] requires = [ - "setuptools==69.0.3", + "setuptools==69.1.1", "setuptools_scm[toml]>=7.1" ] build-backend = "setuptools.build_meta" From ddc3fd500a1bdbc4c3b8af1e48630787116ece3e Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 5 Mar 2024 06:24:56 -0500 Subject: [PATCH 19/35] build: update ruff and settings changes (#236) --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e228123..5fbe5c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -166,7 +166,7 @@ extend-exclude = [ "tests", ] # Follow ST063 - Maintaining and updating linting specifications for updating these. -select = [ # Base linting rule selections. +lint.select = [ # Base linting rule selections. # See the internal document for discussion: # https://docs.google.com/document/d/1i1n8pDmFmWi4wTDpk-JfnWCVUThPJiggyPi2DYwBBu4/edit # All sections here are stable in ruff and shouldn't randomly introduce @@ -202,11 +202,11 @@ select = [ # Base linting rule selections. "PL", # Pylint "TRY", # Cleaner try/except, ] -extend-select = [ +lint.extend-select = [ # Pyupgrade: https://github.com/charliermarsh/ruff#pyupgrade-up "UP00", "UP01", "UP02", "UP030", "UP032", "UP033", # "UP034", # Very new, not yet enabled in ruff 0.0.227 - # Annotations: https://github.com/charliermarsh/ruff#flake8-annotations-ann + # Annotations: https://github.com/charliermarsh/ruff#lint.flake8-annotations-ann "ANN0", # Type annotations for arguments other than `self` and `cls` "ANN2", # Return type annotations "B026", # Keyword arguments must come after starred arguments @@ -228,7 +228,7 @@ extend-select = [ "RUF008", # Do not use mutable default values for dataclass attributes "RUF100", # #noqa directive that doesn't flag anything ] -ignore = [ +lint.ignore = [ "ANN10", # Type annotations for `self` and `cls` #"E203", # Whitespace before ":" -- Commented because ruff doesn't currently check E203 "E501", # Line too long (reason: black will automatically fix this for us) @@ -248,7 +248,7 @@ ignore = [ "ANN401", # Disallow Any in parameters (reason: too restrictive) ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "tests/**.py" = [ # Some things we want for the main project are unnecessary in tests. "D", # Ignore docstring rules in tests "ANN", # Ignore type annotations in tests From 6701822d732e8a31f801af9c6cadb5341f53bc8c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 08:26:32 -0500 Subject: [PATCH 20/35] chore(deps): update dependency lint/ruff to ~=0.3.5 (#246) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5fbe5c6..8ac4158 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dev = [ lint = [ "black==24.2.0", "codespell[toml]==2.2.6", - "ruff~=0.2.1", + "ruff~=0.3.5", "yamllint==1.34.0" ] types = [ From a8d693ffaad60d9e09aee1b102934e0613646254 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:35:40 -0300 Subject: [PATCH 21/35] chore(deps): update development dependencies (non-major) (#244) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8ac4158..ff5d656 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,21 +43,21 @@ emitter = "craft_cli.pytest_plugin" [project.optional-dependencies] dev = [ - "coverage[toml]==7.4.1", - "pytest==8.0.0", + "coverage[toml]==7.4.4", + "pytest==8.1.1", "pytest-cov==4.1.0", - "pytest-mock==3.12.0", + "pytest-mock==3.14.0", "pytest-subprocess" ] lint = [ - "black==24.2.0", + "black==24.4.0", "codespell[toml]==2.2.6", "ruff~=0.3.5", - "yamllint==1.34.0" + "yamllint==1.35.1" ] types = [ - "mypy[reports]==1.8.0", - "pyright==1.1.350", + "mypy[reports]==1.9.0", + "pyright==1.1.358", "types-Pygments", "types-colorama", "types-setuptools", From 67904ef917bb4e03db76c8cc8f5f58cb86145eb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:36:34 -0300 Subject: [PATCH 22/35] chore(deps): update dependency setuptools to v69.5.1 (#247) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ff5d656..6321805 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ types = [ [build-system] requires = [ - "setuptools==69.1.1", + "setuptools==69.5.1", "setuptools_scm[toml]>=7.1" ] build-backend = "setuptools.build_meta" From 244cb87d7d12a31a17120a4588b8e70523285e86 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:41:00 -0300 Subject: [PATCH 23/35] chore(deps): update dependency dev/pytest-cov to v5 (#245) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6321805..7224379 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ emitter = "craft_cli.pytest_plugin" dev = [ "coverage[toml]==7.4.4", "pytest==8.1.1", - "pytest-cov==4.1.0", + "pytest-cov==5.0.0", "pytest-mock==3.14.0", "pytest-subprocess" ] From 22699a56113971ffd1418c09fa9557b94353aa1f Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Wed, 8 Nov 2023 16:16:14 +0200 Subject: [PATCH 24/35] docs: use canonical-sphinx extension This commit removes all sphinx-docs-starter-pack code that was "integrated" into craft-cli's in favor of the canonical-sphinx extension. No functional changes to the docs, other than pulling in newer assets. --- docs/.readthedocs.yaml | 5 +- docs/.sphinx/_static/custom.css | 189 ----------------- docs/.sphinx/_static/favicon.png | Bin 57806 -> 0 bytes docs/.sphinx/_static/furo_colors.css | 88 -------- docs/.sphinx/_static/github_issue_links.css | 24 --- docs/.sphinx/_static/github_issue_links.js | 34 --- docs/.sphinx/_static/header-nav.js | 10 - docs/.sphinx/_static/header.css | 167 --------------- docs/.sphinx/_static/tag.png | Bin 6781 -> 0 bytes docs/.sphinx/_templates/base.html | 12 -- docs/.sphinx/_templates/footer.html | 99 --------- docs/.sphinx/_templates/header.html | 36 ---- docs/.sphinx/_templates/page.html | 49 ----- docs/.sphinx/requirements.txt | 18 -- docs/Makefile | 1 + docs/_static/.gitempty | 0 docs/_static/css/custom.css | 28 --- docs/conf.py | 170 +++++---------- docs/custom_conf.py | 220 -------------------- docs/make.bat | 35 ---- docs/requirements.txt | 5 + tox.ini | 2 +- 22 files changed, 60 insertions(+), 1132 deletions(-) delete mode 100644 docs/.sphinx/_static/custom.css delete mode 100644 docs/.sphinx/_static/favicon.png delete mode 100644 docs/.sphinx/_static/furo_colors.css delete mode 100644 docs/.sphinx/_static/github_issue_links.css delete mode 100644 docs/.sphinx/_static/github_issue_links.js delete mode 100644 docs/.sphinx/_static/header-nav.js delete mode 100644 docs/.sphinx/_static/header.css delete mode 100644 docs/.sphinx/_static/tag.png delete mode 100644 docs/.sphinx/_templates/base.html delete mode 100644 docs/.sphinx/_templates/footer.html delete mode 100644 docs/.sphinx/_templates/header.html delete mode 100644 docs/.sphinx/_templates/page.html delete mode 100644 docs/.sphinx/requirements.txt delete mode 100644 docs/_static/.gitempty delete mode 100644 docs/_static/css/custom.css delete mode 100644 docs/custom_conf.py delete mode 100644 docs/make.bat create mode 100644 docs/requirements.txt diff --git a/docs/.readthedocs.yaml b/docs/.readthedocs.yaml index a1b6ed1..6dee65e 100644 --- a/docs/.readthedocs.yaml +++ b/docs/.readthedocs.yaml @@ -20,4 +20,7 @@ sphinx: # Optionally declare the Python requirements required to build your docs python: install: - - requirements: docs/.sphinx/requirements.txt + - requirements: docs/requirements.txt + - method: pip + path: . + diff --git a/docs/.sphinx/_static/custom.css b/docs/.sphinx/_static/custom.css deleted file mode 100644 index cad94b7..0000000 --- a/docs/.sphinx/_static/custom.css +++ /dev/null @@ -1,189 +0,0 @@ -/** Fix the font weight (300 for normal, 400 for slightly bold) **/ - -div.page, h1, h2, h3, h4, h5, h6, .sidebar-tree .current-page>.reference, button, input, optgroup, select, textarea, th.head { - font-weight: 300 -} - -.toc-tree li.scroll-current>.reference, dl.glossary dt, dl.simple dt, dl:not([class]) dt { - font-weight: 400; -} - -/** Table styling **/ - -th.head { - text-transform: uppercase; - font-size: var(--font-size--small); -} - -table.docutils { - border: 0; - box-shadow: none; - width:100%; -} - -table.docutils td, table.docutils th, table.docutils td:last-child, table.docutils th:last-child, table.docutils td:first-child, table.docutils th:first-child { - border-right: none; - border-left: none; -} - -/* Allow to centre text horizontally in table data cells */ -table.align-center { - text-align: center !important; -} - -/** No rounded corners **/ - -.admonition, code.literal, .sphinx-tabs-tab, .sphinx-tabs-panel, .highlight { - border-radius: 0; -} - -/** Admonition styling **/ - -.admonition { - border-top: 1px solid #d9d9d9; - border-right: 1px solid #d9d9d9; - border-bottom: 1px solid #d9d9d9; -} - -/** Color for the "copy link" symbol next to headings **/ - -a.headerlink { - color: var(--color-brand-primary); -} - -/** Line to the left of the current navigation entry **/ - -.sidebar-tree li.current-page { - border-left: 2px solid var(--color-brand-primary); -} - -/** Some tweaks for issue #16 **/ - -[role="tablist"] { - border-bottom: 1px solid var(--color-sidebar-item-background--hover); -} - -.sphinx-tabs-tab[aria-selected="true"] { - border: 0; - border-bottom: 2px solid var(--color-brand-primary); - background-color: var(--color-sidebar-item-background--current); - font-weight:300; -} - -.sphinx-tabs-tab{ - color: var(--color-brand-primary); - font-weight:300; -} - -.sphinx-tabs-panel { - border: 0; - border-bottom: 1px solid var(--color-sidebar-item-background--hover); - background: var(--color-background-primary); -} - -button.sphinx-tabs-tab:hover { - background-color: var(--color-sidebar-item-background--hover); -} - -/** Custom classes to fix scrolling in tables by decreasing the - font size or breaking certain columns. - Specify the classes in the Markdown file with, for example: - ```{rst-class} break-col-4 min-width-4-8 - ``` -**/ - -table.dec-font-size { - font-size: smaller; -} -table.break-col-1 td.text-left:first-child { - word-break: break-word; -} -table.break-col-4 td.text-left:nth-child(4) { - word-break: break-word; -} -table.min-width-1-15 td.text-left:first-child { - min-width: 15em; -} -table.min-width-4-8 td.text-left:nth-child(4) { - min-width: 8em; -} - -/** Underline for abbreviations **/ - -abbr[title] { - text-decoration: underline solid #cdcdcd; -} - -/** Use the same style for right-details as for left-details **/ -.bottom-of-page .right-details { - font-size: var(--font-size--small); - display: block; -} - -/** Version switcher */ -button.version_select { - color: var(--color-foreground-primary); - background-color: var(--color-toc-background); - padding: 5px 10px; - border: none; -} - -.version_select:hover, .version_select:focus { - background-color: var(--color-sidebar-item-background--hover); -} - -.version_dropdown { - position: relative; - display: inline-block; - text-align: right; - font-size: var(--sidebar-item-font-size); -} - -.available_versions { - display: none; - position: absolute; - right: 0px; - background-color: var(--color-toc-background); - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - z-index: 11; -} - -.available_versions a { - color: var(--color-foreground-primary); - padding: 12px 16px; - text-decoration: none; - display: block; -} - -.available_versions a:hover {background-color: var(--color-sidebar-item-background--current)} - -.show {display:block;} - -/** Fix for nested numbered list - the nested list is lettered **/ -ol.arabic ol.arabic { - list-style: lower-alpha; -} - -/** Make expandable sections look like links **/ -details summary { - color: var(--color-link); -} - -/** Fix the styling of the version box for readthedocs **/ - -#furo-readthedocs-versions .rst-versions, #furo-readthedocs-versions .rst-current-version, #furo-readthedocs-versions:focus-within .rst-current-version, #furo-readthedocs-versions:hover .rst-current-version { - background: var(--color-sidebar-item-background--hover); -} - -.rst-versions .rst-other-versions dd a { - color: var(--color-link); -} - -#furo-readthedocs-versions:focus-within .rst-current-version .fa-book, #furo-readthedocs-versions:hover .rst-current-version .fa-book, .rst-versions .rst-other-versions { - color: var(--color-sidebar-link-text); -} - -.rst-versions .rst-current-version { - color: var(--color-version-popup); - font-weight: bolder; -} diff --git a/docs/.sphinx/_static/favicon.png b/docs/.sphinx/_static/favicon.png deleted file mode 100644 index c7109908f2af5c9bb0ad130c13ac143929643aa2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57806 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

&o&1XroP74a!84^ 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

x=_8 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 Tci{bhEwz{WC?{4XqVNA-TM;k( diff --git a/docs/.sphinx/_static/furo_colors.css b/docs/.sphinx/_static/furo_colors.css deleted file mode 100644 index ffc36cb..0000000 --- a/docs/.sphinx/_static/furo_colors.css +++ /dev/null @@ -1,88 +0,0 @@ -body { - --color-code-background: #f8f8f8; - --color-code-foreground: black; - --font-stack: Ubuntu, -apple-system, Segoe UI, Roboto, Oxygen, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; - --font-stack--monospace: Ubuntu Mono, Consolas, Monaco, Courier, monospace; - --color-foreground-primary: #111; - --color-foreground-secondary: var(--color-foreground-primary); - --color-foreground-muted: #333; - --color-background-secondary: #FFF; - --color-background-hover: #f2f2f2; - --color-brand-primary: #111; - --color-brand-content: #06C; - --color-api-background: #cdcdcd; - --color-inline-code-background: rgba(0,0,0,.03); - --color-sidebar-link-text: #111; - --color-sidebar-item-background--current: #ebebeb; - --color-sidebar-item-background--hover: #f2f2f2; - --toc-font-size: var(--font-size--small); - --color-admonition-title-background--note: var(--color-background-primary); - --color-admonition-title-background--tip: var(--color-background-primary); - --color-admonition-title-background--important: var(--color-background-primary); - --color-admonition-title-background--caution: var(--color-background-primary); - --color-admonition-title--note: #24598F; - --color-admonition-title--tip: #24598F; - --color-admonition-title--important: #C7162B; - --color-admonition-title--caution: #F99B11; - --color-highlighted-background: #EbEbEb; - --color-link-underline: var(--color-background-primary); - --color-link-underline--hover: var(--color-background-primary); - --color-version-popup: #772953; -} - -@media not print { - body[data-theme="dark"] { - --color-code-background: #202020; - --color-code-foreground: #d0d0d0; - --color-foreground-secondary: var(--color-foreground-primary); - --color-foreground-muted: #CDCDCD; - --color-background-secondary: var(--color-background-primary); - --color-background-hover: #666; - --color-brand-primary: #fff; - --color-brand-content: #06C; - --color-sidebar-link-text: #f7f7f7; - --color-sidebar-item-background--current: #666; - --color-sidebar-item-background--hover: #333; - --color-admonition-background: transparent; - --color-admonition-title-background--note: var(--color-background-primary); - --color-admonition-title-background--tip: var(--color-background-primary); - --color-admonition-title-background--important: var(--color-background-primary); - --color-admonition-title-background--caution: var(--color-background-primary); - --color-admonition-title--note: #24598F; - --color-admonition-title--tip: #24598F; - --color-admonition-title--important: #C7162B; - --color-admonition-title--caution: #F99B11; - --color-highlighted-background: #666; - --color-link-underline: var(--color-background-primary); - --color-link-underline--hover: var(--color-background-primary); - --color-version-popup: #F29879; - } - @media (prefers-color-scheme: dark) { - body:not([data-theme="light"]) { - --color-code-background: #202020; - --color-code-foreground: #d0d0d0; - --color-foreground-secondary: var(--color-foreground-primary); - --color-foreground-muted: #CDCDCD; - --color-background-secondary: var(--color-background-primary); - --color-background-hover: #666; - --color-brand-primary: #fff; - --color-brand-content: #06C; - --color-sidebar-link-text: #f7f7f7; - --color-sidebar-item-background--current: #666; - --color-sidebar-item-background--hover: #333; - --color-admonition-background: transparent; - --color-admonition-title-background--note: var(--color-background-primary); - --color-admonition-title-background--tip: var(--color-background-primary); - --color-admonition-title-background--important: var(--color-background-primary); - --color-admonition-title-background--caution: var(--color-background-primary); - --color-admonition-title--note: #24598F; - --color-admonition-title--tip: #24598F; - --color-admonition-title--important: #C7162B; - --color-admonition-title--caution: #F99B11; - --color-highlighted-background: #666; - --color-link-underline: var(--color-background-primary); - --color-link-underline--hover: var(--color-background-primary); - --color-version-popup: #F29879; - } - } -} diff --git a/docs/.sphinx/_static/github_issue_links.css b/docs/.sphinx/_static/github_issue_links.css deleted file mode 100644 index af4be86..0000000 --- a/docs/.sphinx/_static/github_issue_links.css +++ /dev/null @@ -1,24 +0,0 @@ -.github-issue-link-container { - padding-right: 0.5rem; -} -.github-issue-link { - font-size: var(--font-size--small); - font-weight: bold; - background-color: #DD4814; - padding: 13px 23px; - text-decoration: none; -} -.github-issue-link:link { - color: #FFFFFF; -} -.github-issue-link:visited { - color: #FFFFFF -} -.muted-link.github-issue-link:hover { - color: #FFFFFF; - text-decoration: underline; -} -.github-issue-link:active { - color: #FFFFFF; - text-decoration: underline; -} diff --git a/docs/.sphinx/_static/github_issue_links.js b/docs/.sphinx/_static/github_issue_links.js deleted file mode 100644 index f070603..0000000 --- a/docs/.sphinx/_static/github_issue_links.js +++ /dev/null @@ -1,34 +0,0 @@ -// if we already have an onload function, save that one -var prev_handler = window.onload; - -window.onload = function() { - // call the previous onload function - if (prev_handler) { - prev_handler(); - } - - const link = document.createElement("a"); - link.classList.add("muted-link"); - link.classList.add("github-issue-link"); - link.text = "Give feedback"; - link.href = ( - github_url - + "/issues/new?" - + "title=docs%3A+TYPE+YOUR+QUESTION+HERE" - + "&body=*Please describe the question or issue you're facing with " - + `"${document.title}"` - + ".*" - + "%0A%0A%0A%0A%0A" - + "---" - + "%0A" - + `*Reported+from%3A+${location.href}*` - ); - link.target = "_blank"; - - const div = document.createElement("div"); - div.classList.add("github-issue-link-container"); - div.append(link) - - const container = document.querySelector(".article-container > .content-icon-container"); - container.prepend(div); -}; diff --git a/docs/.sphinx/_static/header-nav.js b/docs/.sphinx/_static/header-nav.js deleted file mode 100644 index 3608576..0000000 --- a/docs/.sphinx/_static/header-nav.js +++ /dev/null @@ -1,10 +0,0 @@ -$(document).ready(function() { - $(document).on("click", function () { - $(".more-links-dropdown").hide(); - }); - - $('.nav-more-links').click(function(event) { - $('.more-links-dropdown').toggle(); - event.stopPropagation(); - }); -}) diff --git a/docs/.sphinx/_static/header.css b/docs/.sphinx/_static/header.css deleted file mode 100644 index 0b94409..0000000 --- a/docs/.sphinx/_static/header.css +++ /dev/null @@ -1,167 +0,0 @@ -.p-navigation { - border-bottom: 1px solid var(--color-sidebar-background-border); -} - -.p-navigation__nav { - background: #333333; - display: flex; -} - -.p-logo { - display: flex !important; - padding-top: 0 !important; - text-decoration: none; -} - -.p-logo-image { - height: 44px; - padding-right: 10px; -} - -.p-logo-text { - margin-top: 18px; - color: white; - text-decoration: none; -} - -ul.p-navigation__links { - display: flex; - list-style: none; - margin-left: 0; - margin-top: auto; - margin-bottom: auto; - max-width: 800px; - width: 100%; -} - -ul.p-navigation__links li { - margin: 0 auto; - text-align: center; - width: 100%; -} - -ul.p-navigation__links li a { - background-color: rgba(0, 0, 0, 0); - border: none; - border-radius: 0; - color: var(--color-sidebar-link-text); - display: block; - font-weight: 400; - line-height: 1.5rem; - margin: 0; - overflow: hidden; - padding: 1rem 0; - position: relative; - text-align: left; - text-overflow: ellipsis; - transition-duration: .1s; - transition-property: background-color, color, opacity; - transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - white-space: nowrap; - width: 100%; -} - -ul.p-navigation__links .p-navigation__link { - color: #ffffff; - font-weight: 300; - text-align: center; - text-decoration: none; -} - -ul.p-navigation__links .p-navigation__link:hover { - background-color: #2b2b2b; -} - -ul.p-navigation__links .p-dropdown__link:hover { - background-color: var(--color-sidebar-item-background--hover); -} - -ul.p-navigation__links .p-navigation__sub-link { - background: var(--color-background-primary); - padding: .5rem 0 .5rem .5rem; - font-weight: 300; -} - -ul.p-navigation__links .more-links-dropdown li a { - border-left: 1px solid var(--color-sidebar-background-border); - border-right: 1px solid var(--color-sidebar-background-border); -} - -ul.p-navigation__links .more-links-dropdown li:first-child a { - border-top: 1px solid var(--color-sidebar-background-border); -} - -ul.p-navigation__links .more-links-dropdown li:last-child a { - border-bottom: 1px solid var(--color-sidebar-background-border); -} - -ul.p-navigation__links .p-navigation__logo { - padding: 0.5rem; -} - -ul.p-navigation__links .p-navigation__logo img { - width: 40px; -} - -ul.more-links-dropdown { - display: none; - overflow-x: visible; - height: 0; - z-index: 55; - padding: 0; - position: relative; - list-style: none; - margin-bottom: 0; - margin-top: 0; -} - -.nav-more-links::after { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23111' d='M8.187 11.748l6.187-6.187-1.06-1.061-5.127 5.127L3.061 4.5 2 5.561z'/%3E%3C/svg%3E"); - background-position: center; - background-repeat: no-repeat; - background-size: contain; - content: ""; - display: block; - filter: invert(100%); - height: 1rem; - pointer-events: none; - position: absolute; - right: 1rem; - text-indent: calc(100% + 10rem); - top: calc(1rem + 0.25rem); - width: 1rem; -} - -.nav-ubuntu-com { - display: none; -} - -@media only screen and (min-width: 480px) { - ul.p-navigation__links li { - width: 100%; - } - - .nav-ubuntu-com { - display: inherit; - } -} - -@media only screen and (max-width: 800px) { - .nav-more-links { - margin-left: auto !important; - padding-right: 2rem !important; - width: 8rem !important; - } -} - -@media only screen and (min-width: 800px) { - ul.p-navigation__links li { - width: 100% !important; - } -} - -@media only screen and (min-width: 1310px) { - ul.p-navigation__links { - margin-left: calc(50% - 41em); - } -} diff --git a/docs/.sphinx/_static/tag.png b/docs/.sphinx/_static/tag.png deleted file mode 100644 index f6f6e5aa4bc55fb934c973726b10a0efc92445a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6781 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 diff --git a/docs/.sphinx/_templates/base.html b/docs/.sphinx/_templates/base.html deleted file mode 100644 index 3308154..0000000 --- a/docs/.sphinx/_templates/base.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "furo/base.html" %} - -{% block theme_scripts %} - -{% endblock theme_scripts %} - -{# ru-fu: don't include the color variables from the conf.py file, but use a - separate CSS file to save space #} -{% block theme_styles %} -{% endblock theme_styles %} diff --git a/docs/.sphinx/_templates/footer.html b/docs/.sphinx/_templates/footer.html deleted file mode 100644 index f13cb63..0000000 --- a/docs/.sphinx/_templates/footer.html +++ /dev/null @@ -1,99 +0,0 @@ -{# ru-fu: copied from Furo, with modifications as stated below. Modifications are marked 'mod:'. #} - -

-
-
- {%- if show_copyright %} - - {%- endif %} - - {# mod: removed "Made with" #} - - {%- if last_updated -%} -
- {% trans last_updated=last_updated|e -%} - Last updated on {{ last_updated }} - {%- endtrans -%} -
- {%- endif %} - - {%- if show_source and has_source and sourcename %} - - {%- endif %} -
-
- - {# mod: replaced RTD icons with our links #} - - {% if discourse %} - - {% endif %} - - {% if github_url and github_version and github_folder %} - - {% if github_issues %} - - {% endif %} - - - {% endif %} - - -
-
- diff --git a/docs/.sphinx/_templates/header.html b/docs/.sphinx/_templates/header.html deleted file mode 100644 index 1a128b6..0000000 --- a/docs/.sphinx/_templates/header.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/docs/.sphinx/_templates/page.html b/docs/.sphinx/_templates/page.html deleted file mode 100644 index bda3061..0000000 --- a/docs/.sphinx/_templates/page.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends "furo/page.html" %} - -{% block footer %} - {% include "footer.html" %} -{% endblock footer %} - -{% block body -%} - {% include "header.html" %} - {{ super() }} -{%- endblock body %} - -{% if meta and ((meta.discourse and discourse_prefix) or meta.relatedlinks) %} - {% set furo_hide_toc_orig = furo_hide_toc %} - {% set furo_hide_toc=false %} -{% endif %} - -{% block right_sidebar %} -
- {% if not furo_hide_toc_orig %} -
- - {{ _("Contents") }} - -
-
-
- {{ toc }} -
-
- {% endif %} - {% if meta and ((meta.discourse and discourse_prefix) or meta.relatedlinks) %} - - - {% endif %} -
-{% endblock right_sidebar %} diff --git a/docs/.sphinx/requirements.txt b/docs/.sphinx/requirements.txt deleted file mode 100644 index 8e49bd4..0000000 --- a/docs/.sphinx/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -furo -linkify-it-py -lxd-sphinx-extensions -myst-parser -pyspelling -sphinx -sphinx-autobuild -sphinx-copybutton -sphinx-design -sphinx-notfound-page -sphinx-reredirects -sphinx-tabs -sphinxcontrib-jquery -sphinxext-opengraph -# Extra, craft-cli-specific requirements for the docs -sphinx-toolbox==3.5.0 -sphinx-lint==0.9.1 -pytest>=7.0.0 # This is just because this is imported by the code diff --git a/docs/Makefile b/docs/Makefile index a76d8aa..9afa47a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -87,3 +87,4 @@ woke: woke-install # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile . $(VENV); $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + diff --git a/docs/_static/.gitempty b/docs/_static/.gitempty deleted file mode 100644 index e69de29..0000000 diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css deleted file mode 100644 index ef7e97f..0000000 --- a/docs/_static/css/custom.css +++ /dev/null @@ -1,28 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Ubuntu:ital@0;1&display=swap'); - -body { - font-family: Ubuntu, "times new roman", times, roman, serif; -} - -div .toctree-wrapper { - column-count: 2; -} - -div .toctree-wrapper>ul { - margin: 0; -} - -ul .toctree-l1 { - margin: 0; - -webkit-column-break-inside: avoid; - page-break-inside: avoid; - break-inside: avoid-column; -} - -.wy-nav-content { - max-width: none; -} - -.log-snippets { - color: rgb(141, 141, 141); -} diff --git a/docs/conf.py b/docs/conf.py index 3292d81..db33e3d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,139 +1,67 @@ -import sys +import datetime -sys.path.append('./') -from custom_conf import * +project = "Craft CLI" +author = "Canonical Group Ltd" +html_title = project + " documentation" +copyright = "%s, %s" % (datetime.date.today().year, author) -# Configuration file for the Sphinx documentation builder. -# You should not do any modifications to this file. Put your custom -# configuration into the custom_conf.py file. -# If you need to change this file, contribute the changes upstream. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html +ogp_site_url = "https://canonical-craft-cli.readthedocs-hosted.com/" +ogp_site_name = project +ogp_image = "https://assets.ubuntu.com/v1/253da317-image-document-ubuntudocs.svg" -############################################################ -### Extensions -############################################################ +html_context = { + "product_page": "github.com/canonical/craft-cli", + "github_url": "https://github.com/canonical/craft-cli", +} + +linkcheck_ignore = ["craft_cli.dispatcher.html#craft_cli.dispatcher.CommandGroup"] +# Add extensions extensions = [ - 'sphinx_design', - 'sphinx_tabs.tabs', - 'sphinx_reredirects', - 'youtube-links', - 'related-links', - 'custom-rst-roles', - 'terminal-output', - 'sphinx_copybutton', - 'sphinxext.opengraph', - 'myst_parser', - 'sphinxcontrib.jquery', - 'notfound.extension' + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.autodoc", + "sphinx_toolbox", + "canonical_sphinx", ] -extensions.extend(custom_extensions) -### Configuration for extensions +# Type hints configuration +set_type_checking_flag = True +typehints_fully_qualified = False +always_document_param_types = True +typehints_document_rtype = True -# Additional MyST syntax -myst_enable_extensions = [ - 'substitution', - 'deflist', - 'linkify' -] -myst_enable_extensions.extend(custom_myst_extensions) +github_username = "canonical" +github_repository = "craft-cli" -# Used for related links -if not 'discourse_prefix' in html_context and 'discourse' in html_context: - html_context['discourse_prefix'] = html_context['discourse'] + '/t/' +# Document class properties before public methods +autodoc_member_order = "bysource" -# The default for notfound_urls_prefix usually works, but not for -# documentation on documentation.ubuntu.com -if slug: - notfound_urls_prefix = '/' + slug + '/en/latest/' -notfound_context = { - 'title': 'Page not found', - 'body': '

Page not found

\n\n

Sorry, but the documentation page that you are looking for was not found.

\n

Documentation changes over time, and pages are moved around. We try to redirect you to the updated content where possible, but unfortunately, that didn\'t work this time (maybe because the content you were looking for does not exist in this version of the documentation).

\n

You can try to use the navigation to locate the content you\'re looking for, or search for a similar page.

\n', -} +# region Setup reference generation +def run_apidoc(_): + from sphinx.ext.apidoc import main + import os + import sys -# Default image for OGP (to prevent font errors, see -# https://github.com/canonical/sphinx-docs-starter-pack/pull/54 ) -if not 'ogp_image' in locals(): - ogp_image = 'https://assets.ubuntu.com/v1/253da317-image-document-ubuntudocs.svg' + sys.path.append(os.path.join(os.path.dirname(__file__), "..")) + cur_dir = os.path.abspath(os.path.dirname(__file__)) + module = os.path.join(cur_dir, "..", "craft_cli") + exclude_patterns = ["*pytest_plugin*"] + main(["-e", "--no-toc", "--force", "-o", cur_dir, module, *exclude_patterns]) -############################################################ -### General configuration -############################################################ -exclude_patterns = [ - '_build', - 'Thumbs.db', - '.DS_Store', - '.sphinx', -] -exclude_patterns.extend(custom_excludes) +def no_namedtuple_attrib_docstring(app, what, name, obj, options, lines): + """Strips out silly "Alias for field number" lines in namedtuples reference.""" + if len(lines) == 1 and lines[0].startswith("Alias for field number"): + del lines[:] -rst_epilog = ''' -.. include:: /reuse/links.txt -''' -if 'custom_rst_epilog' in locals(): - rst_epilog = custom_rst_epilog -source_suffix = { - '.rst': 'restructuredtext', - '.md': 'markdown', -} - -if not 'conf_py_path' in html_context and 'github_folder' in html_context: - html_context['conf_py_path'] = html_context['github_folder'] +def setup(app): + app.connect("builder-inited", run_apidoc) + app.connect("autodoc-process-docstring", no_namedtuple_attrib_docstring) -# For ignoring specific links -linkcheck_anchors_ignore_for_url = [ - r'https://github\.com/.*' -] -linkcheck_anchors_ignore_for_url.extend(custom_linkcheck_anchors_ignore_for_url) - -# Tags cannot be added directly in custom_conf.py, so add them here -for tag in custom_tags: - tags.add(tag) - -############################################################ -### Styling -############################################################ - -# Find the current builder -builder = 'dirhtml' -if '-b' in sys.argv: - builder = sys.argv[sys.argv.index('-b')+1] - -# Setting templates_path for epub makes the build fail -if builder == 'dirhtml' or builder == 'html': - templates_path = ['.sphinx/_templates'] - -# Theme configuration -html_theme = 'furo' -html_last_updated_fmt = '' -html_permalinks_icon = '¶' - -if html_title == '': - html_theme_options = { - 'sidebar_hide_name': True - } - -############################################################ -### Additional files -############################################################ - -html_static_path = ['.sphinx/_static'] - -html_css_files = [ - 'custom.css', - 'header.css', - 'github_issue_links.css', - 'furo_colors.css' -] -html_css_files.extend(custom_html_css_files) -html_js_files = ['header-nav.js'] -if 'github_issues' in html_context and html_context['github_issues'] and not disable_feedback_button: - html_js_files.append('github_issue_links.js') -html_js_files.extend(custom_html_js_files) +# endregion diff --git a/docs/custom_conf.py b/docs/custom_conf.py deleted file mode 100644 index 62b9fea..0000000 --- a/docs/custom_conf.py +++ /dev/null @@ -1,220 +0,0 @@ -import datetime - -# Custom configuration for the Sphinx documentation builder. -# All configuration specific to your project should be done in this file. -# -# The file is included in the common conf.py configuration file. -# You can modify any of the settings below or add any configuration that -# is not covered by the common conf.py file. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html -# -# If you're not familiar with Sphinx and don't want to use advanced -# features, it is sufficient to update the settings in the "Project -# information" section. - -############################################################ -### Project information -############################################################ - -# Product name -project = "Craft CLI" -author = "Canonical Group Ltd" - -# The title you want to display for the documentation in the sidebar. -# You might want to include a version number here. -# To not display any title, set this option to an empty string. -html_title = project + " documentation" - -# The default value uses the current year as the copyright year. -# -# For static works, it is common to provide the year of first publication. -# Another option is to give the first year and the current year -# for documentation that is often changed, e.g. 2022–2023 (note the en-dash). -# -# A way to check a GitHub repo's creation date is to obtain a classic GitHub -# token with 'repo' permissions here: https://github.com/settings/tokens -# Next, use 'curl' and 'jq' to extract the date from the GitHub API's output: -# -# curl -H 'Authorization: token ' \ -# -H 'Accept: application/vnd.github.v3.raw' \ -# https://api.github.com/repos/canonical/ | jq '.created_at' - -copyright = "%s, %s" % (datetime.date.today().year, author) - -## Open Graph configuration - defines what is displayed as a link preview -## when linking to the documentation from another website (see https://ogp.me/) -# The URL where the documentation will be hosted (leave empty if you -# don't know yet) -ogp_site_url = "https://canonical-craft-cli.readthedocs-hosted.com/" -# The documentation website name (usually the same as the product name) -ogp_site_name = project -# The URL of an image or logo that is used in the preview -ogp_image = "https://assets.ubuntu.com/v1/253da317-image-document-ubuntudocs.svg" - -# Update with the local path to the favicon for your product -# (default is the circle of friends) -html_favicon = ".sphinx/_static/favicon.png" - -# (Some settings must be part of the html_context dictionary, while others -# are on root level. Don't move the settings.) -html_context = { - # Change to the link to the website of your product (without "https://") - # For example: "ubuntu.com/lxd" or "microcloud.is" - # If there is no product website, edit the header template to remove the - # link (see the readme for instructions). - "product_page": "github.com/canonical/craft-cli", - # Add your product tag (the orange part of your logo, will be used in the - # header) to ".sphinx/_static" and change the path here (start with "_static") - # (default is the circle of friends) - "product_tag": "_static/tag.png", - # Change to the discourse instance you want to be able to link to - # using the :discourse: metadata at the top of a file - # (use an empty value if you don't want to link) - "discourse": "https://discourse.ubuntu.com", - # Change to the GitHub URL for your project - "github_url": "https://github.com/canonical/craft-cli", - # Change to the branch for this version of the documentation - "github_version": "main", - # Change to the folder that contains the documentation - # (usually "/" or "/docs/") - "github_folder": "/docs/", - # Change to an empty value if your GitHub repo doesn't have issues enabled. - # This will disable the feedback button and the issue link in the footer. - "github_issues": "enabled", - # Controls the existence of Previous / Next buttons at the bottom of pages - # Valid options: none, prev, next, both - "sequential_nav": "none", -} - -# If your project is on documentation.ubuntu.com, specify the project -# slug (for example, "lxd") here. -slug = "" - -############################################################ -### Redirects -############################################################ - -# Set up redirects (https://documatt.gitlab.io/sphinx-reredirects/usage.html) -# For example: 'explanation/old-name.html': '../how-to/prettify.html', - -redirects = {} - -############################################################ -### Link checker exceptions -############################################################ - -# Links to ignore when checking links - -linkcheck_ignore = ["http://127.0.0.1:8000"] - -# Pages on which to ignore anchors -# (This list will be appended to linkcheck_anchors_ignore_for_url) - -custom_linkcheck_anchors_ignore_for_url = [] - -############################################################ -### Additions to default configuration -############################################################ - -## The following settings are appended to the default configuration. -## Use them to extend the default functionality. - -# Add extensions -custom_extensions = [ - "sphinx.ext.intersphinx", - "sphinx.ext.viewcode", - "sphinx.ext.coverage", - "sphinx.ext.doctest", - "sphinx_design", - "sphinx_copybutton", - "sphinx.ext.autodoc", - "sphinx_toolbox", -] - -# Add MyST extensions -custom_myst_extensions = [] - -# Add files or directories that should be excluded from processing. -custom_excludes = [ - "doc-cheat-sheet*", -] - -# Add CSS files (located in .sphinx/_static/) -custom_html_css_files = [] - -# Add JavaScript files (located in .sphinx/_static/) -custom_html_js_files = [] - -## The following settings override the default configuration. - -# Specify a reST string that is included at the end of each file. -# If commented out, use the default (which pulls the reuse/links.txt -# file into each reST file). -# custom_rst_epilog = '' - -# By default, the documentation includes a feedback button at the top. -# You can disable it by setting the following configuration to True. -disable_feedback_button = False - -# Add tags that you want to use for conditional inclusion of text -# (https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#tags) -custom_tags = [] - -############################################################ -### Additional configuration -############################################################ - -## Add any configuration that is not covered by the common conf.py file. - -linkcheck_ignore += [r"craft_cli.dispatcher.html#craft_cli.dispatcher.CommandGroup"] - -# Type hints configuration -set_type_checking_flag = True -typehints_fully_qualified = False -always_document_param_types = True -typehints_document_rtype = True - -# Github config -github_username = "canonical" -github_repository = "craft-cli" -# endregion - -# Document class properties before public methods -autodoc_member_order = "bysource" - - -# region Setup reference generation -def run_apidoc(_): - from pathlib import Path - from sphinx.ext.apidoc import main - import os - import shutil - import sys - - sys.path.append(os.path.join(os.path.dirname(__file__), "..")) - cur_dir = os.path.abspath(os.path.dirname(__file__)) - module = os.path.join(cur_dir, "..", "craft_cli") - exclude_patterns = ["*pytest_plugin*"] - main(["-e", "--no-toc", "--force", "-o", cur_dir, module, *exclude_patterns]) - - # After calling apidoc, replace the one generated for the ``messages`` - # module with our special one that spells out the items to document. - messages = Path(cur_dir) / "autodoc/craft_cli.messages.template" - assert messages.is_file() - shutil.copy(messages, Path(cur_dir) / "craft_cli.messages.rst") - - -def no_namedtuple_attrib_docstring(app, what, name, obj, options, lines): - """Strips out silly "Alias for field number" lines in namedtuples reference.""" - if len(lines) == 1 and lines[0].startswith("Alias for field number"): - del lines[:] - - -def setup(app): - app.connect("builder-inited", run_apidoc) - app.connect("autodoc-process-docstring", no_namedtuple_attrib_docstring) - - -# endregion diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 32bb245..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..4b076e8 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +# craft-cli-specific requirements for the docs +sphinx-toolbox==3.5.0 +sphinx-lint==0.9.1 +pytest>=7.0.0 # This is just because this is imported by the code +canonical-sphinx@git+https://github.com/canonical/canonical-sphinx@CRAFT-2671-update-assets diff --git a/tox.ini b/tox.ini index da7b4a2..feb5fd1 100644 --- a/tox.ini +++ b/tox.ini @@ -106,7 +106,7 @@ commands = [docs] # Sphinx documentation configuration deps = - -r{tox_root}/docs/.sphinx/requirements.txt + -r{tox_root}/docs/requirements.txt package = editable no_package = true env_dir = {work_dir}/docs From 80824ba71405155f3ab067c1e4dfa30e2d2a15b7 Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Tue, 16 Apr 2024 12:47:17 -0300 Subject: [PATCH 25/35] chore: move docs deps to pyproject.toml --- docs/.readthedocs.yaml => .readthedocs.yaml | 6 +++--- docs/.sphinx/requirements.txt | 3 +++ docs/requirements.txt | 5 ----- pyproject.toml | 6 ++++++ tox.ini | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) rename docs/.readthedocs.yaml => .readthedocs.yaml (89%) create mode 100644 docs/.sphinx/requirements.txt delete mode 100644 docs/requirements.txt diff --git a/docs/.readthedocs.yaml b/.readthedocs.yaml similarity index 89% rename from docs/.readthedocs.yaml rename to .readthedocs.yaml index 6dee65e..eacc66f 100644 --- a/docs/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.12" # Build documentation in the docs/ directory with Sphinx sphinx: @@ -20,7 +20,7 @@ sphinx: # Optionally declare the Python requirements required to build your docs python: install: - - requirements: docs/requirements.txt - method: pip path: . - + extra_requirements: + - docs diff --git a/docs/.sphinx/requirements.txt b/docs/.sphinx/requirements.txt new file mode 100644 index 0000000..65b7d53 --- /dev/null +++ b/docs/.sphinx/requirements.txt @@ -0,0 +1,3 @@ +# This file is used by the automatic-doc-checks CI workflow +# This is sourced the "craft-cli/docs" dir, so this installs "craft-cli[docs]" +..[docs] diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 4b076e8..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# craft-cli-specific requirements for the docs -sphinx-toolbox==3.5.0 -sphinx-lint==0.9.1 -pytest>=7.0.0 # This is just because this is imported by the code -canonical-sphinx@git+https://github.com/canonical/canonical-sphinx@CRAFT-2671-update-assets diff --git a/pyproject.toml b/pyproject.toml index 7224379..bfae43a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,12 @@ types = [ "types-colorama", "types-setuptools", ] +docs = [ + "sphinx-toolbox==3.5.0", + "sphinx-lint==0.9.1", + "pytest>=7.0.0", # pytest is imported by the code, which is parsed for api gen. + "canonical-sphinx==0.1.0", +] [build-system] requires = [ diff --git a/tox.ini b/tox.ini index feb5fd1..73cb0d4 100644 --- a/tox.ini +++ b/tox.ini @@ -105,12 +105,12 @@ commands = codespell: codespell --toml {tox_root}/pyproject.toml --write-changes {posargs} [docs] # Sphinx documentation configuration -deps = - -r{tox_root}/docs/requirements.txt +extras = docs package = editable no_package = true env_dir = {work_dir}/docs runner = ignore_env_name_mismatch +source_dir = {tox_root}/{project_name} [testenv:build-docs] description = Build sphinx documentation From ad50596d0b6cd94753689390cd02dab92ed428cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 20:13:51 -0300 Subject: [PATCH 26/35] chore(deps): update dependency types/pyright to v1.1.359 (#251) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bfae43a..1c71289 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ lint = [ ] types = [ "mypy[reports]==1.9.0", - "pyright==1.1.358", + "pyright==1.1.359", "types-Pygments", "types-colorama", "types-setuptools", From 35b6ab3de1ca64bab1d8c70ddeece553abe0eb90 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 23 Apr 2024 07:57:57 -0400 Subject: [PATCH 27/35] chore: switch to ruff snap (#250) --- .github/workflows/tests.yaml | 2 +- pyproject.toml | 1 - tox.ini | 7 ++++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 3432c1d..f62d952 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -25,7 +25,7 @@ jobs: echo "::group::Begin snap install" echo "Installing snaps in the background while running apt and pip..." sudo snap install --no-wait --classic pyright - sudo snap install --no-wait shellcheck + sudo snap install --no-wait ruff shellcheck echo "::endgroup::" echo "::group::pip install" python -m pip install 'tox>=4' tox-gh diff --git a/pyproject.toml b/pyproject.toml index 1c71289..5d406e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,6 @@ dev = [ lint = [ "black==24.4.0", "codespell[toml]==2.2.6", - "ruff~=0.3.5", "yamllint==1.35.1" ] types = [ diff --git a/tox.ini b/tox.ini index 73cb0d4..163b624 100644 --- a/tox.ini +++ b/tox.ini @@ -61,6 +61,9 @@ package = editable extras = lint env_dir = {work_dir}/linting runner = ignore_env_name_mismatch +allowlist_externals = + ruff: ruff + shellcheck: bash, xargs [shellcheck] find = git ls-files @@ -70,8 +73,6 @@ filter = file --mime-type -Nnf- | grep shellscript | cut -f1 -d: description = Lint the source code base = testenv, lint labels = lint -allowlist_externals = - shellcheck: bash, xargs commands_pre = shellcheck: bash -c '{[shellcheck]find} | {[shellcheck]filter} > {env_tmp_dir}/shellcheck_files' commands = @@ -101,7 +102,7 @@ base = testenv, lint labels = format commands = black: black {tty:--color} {posargs} . - ruff: ruff --fix --respect-gitignore {posargs} . + ruff: ruff check --fix --respect-gitignore {posargs} . codespell: codespell --toml {tox_root}/pyproject.toml --write-changes {posargs} [docs] # Sphinx documentation configuration From cd4bb1a60590d312aca6205e1c37e93bdca7902f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 09:49:56 -0300 Subject: [PATCH 28/35] chore(deps): update development dependencies (non-major) (#254) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5d406e7..a1208c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,20 +43,20 @@ emitter = "craft_cli.pytest_plugin" [project.optional-dependencies] dev = [ - "coverage[toml]==7.4.4", - "pytest==8.1.1", + "coverage[toml]==7.5.3", + "pytest==8.2.1", "pytest-cov==5.0.0", "pytest-mock==3.14.0", "pytest-subprocess" ] lint = [ - "black==24.4.0", - "codespell[toml]==2.2.6", + "black==24.4.2", + "codespell[toml]==2.3.0", "yamllint==1.35.1" ] types = [ - "mypy[reports]==1.9.0", - "pyright==1.1.359", + "mypy[reports]==1.10.0", + "pyright==1.1.365", "types-Pygments", "types-colorama", "types-setuptools", From 3d2199866cbf01dbb640972c2f307df81d6c7114 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:28:44 +0000 Subject: [PATCH 29/35] chore(deps): update dependency setuptools to v70 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a1208c7..b33fc54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ docs = [ [build-system] requires = [ - "setuptools==69.5.1", + "setuptools==70.1.0", "setuptools_scm[toml]>=7.1" ] build-backend = "setuptools.build_meta" From 2faeeb7ebbb5d5b07cefc5df33541c754c59291c Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Fri, 21 Jun 2024 14:19:47 -0300 Subject: [PATCH 30/35] feat: split the docs url into base and slug (#257) This commit provides a way to separate the base url for the project's docs and the slug for a specific topic. In particular, this is useful when raising a CraftError so that it holds just the slug of the user documentation for that issue, which is then combined by the Emitter into a full url when reporting the error to the user. --- craft_cli/errors.py | 9 ++++++ craft_cli/messages.py | 17 +++++++++- tests/unit/test_messages_emitter.py | 48 +++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/craft_cli/errors.py b/craft_cli/errors.py index 77c43ac..e21642e 100644 --- a/craft_cli/errors.py +++ b/craft_cli/errors.py @@ -42,6 +42,10 @@ class CraftError(Exception): docs_url: Optional[str] """An URL to point the user to documentation (to be shown together with ``message``).""" + doc_slug: Optional[str] + """The slug to the user documentation. Needs a base url to form a full address. + Note that ``docs_url`` has preference if it is set.""" + logpath_report: bool """Whether the location of the log filepath should be presented in the screen as the final message.""" @@ -62,6 +66,7 @@ def __init__( # noqa: PLR0913 (too many arguments) logpath_report: bool = True, reportable: bool = True, retcode: int = 1, + doc_slug: Optional[str] = None, ) -> None: super().__init__(message) self.details = details @@ -70,6 +75,9 @@ def __init__( # noqa: PLR0913 (too many arguments) self.logpath_report = logpath_report self.reportable = reportable self.retcode = retcode + self.doc_slug = doc_slug + if doc_slug and not doc_slug.startswith("/"): + self.doc_slug = "/" + doc_slug def __eq__(self, other: object) -> bool: if isinstance(other, CraftError): @@ -82,6 +90,7 @@ def __eq__(self, other: object) -> bool: self.logpath_report == other.logpath_report, self.reportable == other.reportable, self.retcode == other.retcode, + self.doc_slug == other.doc_slug, ] ) return NotImplemented diff --git a/craft_cli/messages.py b/craft_cli/messages.py index baffb02..7574f06 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -456,6 +456,7 @@ def __init__(self) -> None: self._log_filepath: pathlib.Path = None # type: ignore[assignment] self._log_handler: _Handler = None # type: ignore[assignment] self._streaming_brief = False + self._docs_base_url: str | None = None def init( # noqa: PLR0913 (too many arguments) self, @@ -465,11 +466,14 @@ def init( # noqa: PLR0913 (too many arguments) log_filepath: pathlib.Path | None = None, *, streaming_brief: bool = False, + docs_base_url: str | None = None, ) -> None: """Initialize the emitter; this must be called once and before emitting any messages. :param streaming_brief: Whether informational messages should be streamed with progress messages when using BRIEF mode (see example 29). + :param docs_base_url: The base address of the documentation, for error reporting + purposes. """ if self._initiated: if TESTMODE: @@ -480,6 +484,10 @@ def init( # noqa: PLR0913 (too many arguments) self._greeting = greeting self._streaming_brief = streaming_brief + self._docs_base_url = docs_base_url + if docs_base_url and docs_base_url.endswith("/"): + self._docs_base_url = docs_base_url[:-1] + # create a log file, bootstrap the printer, and before anything else send the greeting # to the file self._log_filepath = _get_log_filepath(appname) if log_filepath is None else log_filepath @@ -727,8 +735,15 @@ def _report_error(self, error: errors.CraftError) -> None: if error.resolution: text = f"Recommended resolution: {error.resolution}" self._printer.show(sys.stderr, text, use_timestamp=use_timestamp, end_line=True) + + doc_url = None + if self._docs_base_url and error.doc_slug: + doc_url = self._docs_base_url + error.doc_slug if error.docs_url: - text = f"For more information, check out: {error.docs_url}" + doc_url = error.docs_url + + if doc_url: + text = f"For more information, check out: {doc_url}" self._printer.show(sys.stderr, text, use_timestamp=use_timestamp, end_line=True) # expose the logfile path only if indicated diff --git a/tests/unit/test_messages_emitter.py b/tests/unit/test_messages_emitter.py index 0eb5019..c976447 100644 --- a/tests/unit/test_messages_emitter.py +++ b/tests/unit/test_messages_emitter.py @@ -66,9 +66,9 @@ def get_initiated_emitter(tmp_path, monkeypatch): monkeypatch.setattr(messages, "_get_log_filepath", lambda appname: fake_logpath) with patch("craft_cli.messages.Printer", autospec=True) as mock_printer: - def func(mode, greeting="default greeting"): + def func(mode, *, greeting="default greeting", **kwargs): emitter = RecordingEmitter() - emitter.init(mode, "testappname", greeting) + emitter.init(mode, "testappname", greeting, **kwargs) emitter.printer_calls = mock_printer.mock_calls emitter.printer_calls.clear() return emitter @@ -1064,3 +1064,47 @@ def test_reporterror_no_logpath(get_initiated_emitter): call().show(sys.stderr, "test message", use_timestamp=True, end_line=True), call().stop(), ] + + +@pytest.mark.parametrize( + ("docs_base_url", "doc_slug"), + [ + ("https://documentation.ubuntu.com/testcraft", "reference/error.html"), + ("https://documentation.ubuntu.com/testcraft/", "reference/error.html"), + ("https://documentation.ubuntu.com/testcraft", "/reference/error.html"), + ("https://documentation.ubuntu.com/testcraft/", "/reference/error.html"), + ], +) +def test_reporterror_doc_slug(get_initiated_emitter, docs_base_url, doc_slug): + emitter = get_initiated_emitter(EmitterMode.BRIEF, docs_base_url=docs_base_url) + error = CraftError("test message", logpath_report=False, doc_slug=doc_slug) + emitter.error(error) + + full_docs_message = ( + "For more information, check out: " + "https://documentation.ubuntu.com/testcraft/reference/error.html" + ) + assert emitter.printer_calls == [ + call().show(sys.stderr, "test message", use_timestamp=False, end_line=True), + call().show(sys.stderr, full_docs_message, use_timestamp=False, end_line=True), + call().stop(), + ] + + +def test_reporterror_both_url_and_slug(get_initiated_emitter): + docs_base_url = "https://base-url.ubuntu.com/testcraft" + doc_slug = "/slug" + full_url = "https://full-url.ubuntu.com/testcraft/full" + emitter = get_initiated_emitter(EmitterMode.BRIEF, docs_base_url=docs_base_url) + + # An error with both docs_url and doc_slug + error = CraftError("test message", logpath_report=False, docs_url=full_url, doc_slug=doc_slug) + emitter.error(error) + + full_docs_message = f"For more information, check out: {full_url}" + + assert emitter.printer_calls == [ + call().show(sys.stderr, "test message", use_timestamp=False, end_line=True), + call().show(sys.stderr, full_docs_message, use_timestamp=False, end_line=True), + call().stop(), + ] From 2a79b3f1f6d5c7411394277c7eb17bd7e8a01c82 Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Tue, 2 Jul 2024 10:45:55 -0300 Subject: [PATCH 31/35] docs(changelog): add changelog with entries for 2.6.0 --- docs/.custom_wordlist.txt | 3 +++ docs/changelog.rst | 13 +++++++++++++ docs/index.rst | 1 + 3 files changed, 17 insertions(+) create mode 100644 docs/changelog.rst diff --git a/docs/.custom_wordlist.txt b/docs/.custom_wordlist.txt index cb0bf4f..b10c82a 100644 --- a/docs/.custom_wordlist.txt +++ b/docs/.custom_wordlist.txt @@ -2,8 +2,10 @@ appname args ArgumentParser backend +Changelog cli CLI's +CraftError GlobalArgument helptexts logfile @@ -18,4 +20,5 @@ subprocesses sysargs TOs tracebacks +urls UX diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..9398d4e --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,13 @@ +*************** +Changelog +*************** + +See the `Releases page`_ on GitHub for a complete list of commits that are +included in each version. + +2.6.0 (2024-02-07) +------------------ +- Disable exception chaining for help/usage exceptions +- Support a doc slug in CraftError in addition to full urls + +.. _Releases page: https://github.com/canonical/craft-cli/releases diff --git a/docs/index.rst b/docs/index.rst index dcdae15..7fe6cf7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,6 +9,7 @@ Welcome to Craft CLI's documentation! howtos reference explanations + changelog .. grid:: 1 1 2 2 From 6d95a8da970c7fb8fc11953a6a76b53806527b30 Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Wed, 3 Jul 2024 11:07:50 -0300 Subject: [PATCH 32/35] ci: add workflow to release to PyPI (#259) This workflow file is a copy of Starbase's, with the modification to run on self-hosted runners (and to use Python 3.10 instead of 3.11). Co-authored-by: Callahan --- .github/workflows/release-publish.yaml | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/release-publish.yaml diff --git a/.github/workflows/release-publish.yaml b/.github/workflows/release-publish.yaml new file mode 100644 index 0000000..f683c80 --- /dev/null +++ b/.github/workflows/release-publish.yaml @@ -0,0 +1,60 @@ +name: Release +on: + push: + tags: + # These tags should be protected, remember to enable the rule: + # https://github.com/canonical/craft-cli/settings/tag_protection + - "[0-9]+.[0-9]+.[0-9]+" + +permissions: + contents: write + +jobs: + source-wheel: + runs-on: [self-hosted, jammy] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Fetch tag annotations + run: | + git fetch --force --tags --depth 1 + git describe --dirty --long --match '[0-9]*.[0-9]*.[0-9]*' --exclude '*[^0-9.]*' + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + check-latest: true + - name: Build packages + run: | + pip install build twine + python3 -m build + twine check dist/* + - name: Upload pypi packages artifact + uses: actions/upload-artifact@v4 + with: + name: pypi-packages + path: dist/ + pypi: + needs: ["source-wheel"] + runs-on: [self-hosted, jammy] + steps: + - name: Get packages + uses: actions/download-artifact@v4 + with: + name: pypi-packages + path: dist/ + - name: Publish to pypi + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + github-release: + needs: ["source-wheel"] + runs-on: [self-hosted, jammy] + steps: + - name: Get pypi artifacts + uses: actions/download-artifact@v4 + - name: Release + uses: softprops/action-gh-release@v2 + with: + files: | + ** From 4852243194b0e2cdd791e857b15013bf7c59d548 Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Fri, 26 Jul 2024 11:29:59 -0300 Subject: [PATCH 33/35] feat: add CraftCommandError This new error class has a ``stderr`` attribute that holds the error output of an executed command. As the class' docstring indicates, this new attribute is halfway between the ``message`` and the ``details`` parameters, verbosity -wise. It's meant to be used in cases where this error output is expected to be useful enough to the end user to make up for the extra text, even in "brief"-mode executions. Fixes #260 --- craft_cli/__init__.py | 3 ++- craft_cli/errors.py | 33 +++++++++++++++++++++++++++- craft_cli/messages.py | 9 ++++++-- tests/unit/test_errors.py | 29 +++++++++++++++++++++++- tests/unit/test_messages_emitter.py | 34 ++++++++++++++++++++++++++++- 5 files changed, 102 insertions(+), 6 deletions(-) diff --git a/craft_cli/__init__.py b/craft_cli/__init__.py index 18bb8f2..31d8073 100644 --- a/craft_cli/__init__.py +++ b/craft_cli/__init__.py @@ -31,13 +31,14 @@ # is to break cyclic dependencies from .messages import EmitterMode, emit # isort:skip from .dispatcher import BaseCommand, CommandGroup, Dispatcher, GlobalArgument -from .errors import ArgumentParsingError, CraftError, ProvideHelpException +from .errors import ArgumentParsingError, CraftError, CraftCommandError, ProvideHelpException from .helptexts import HIDDEN # noqa: F401 __all__ = [ "ArgumentParsingError", "BaseCommand", "CommandGroup", + "CraftCommandError", "CraftError", "Dispatcher", "EmitterMode", diff --git a/craft_cli/errors.py b/craft_cli/errors.py index e21642e..a7a9bdd 100644 --- a/craft_cli/errors.py +++ b/craft_cli/errors.py @@ -20,7 +20,7 @@ "CraftError", ] -from typing import Optional +from typing import Any, Optional, Union, cast class CraftError(Exception): @@ -96,6 +96,37 @@ def __eq__(self, other: object) -> bool: return NotImplemented +class CraftCommandError(CraftError): + """A CraftError with precise error output from a command. + + This exception class augments CraftError with the addition of a ``stderr`` + parameter. This parameter is meant to hold the standard error contents of + the failed command - as such, it sits between the typically brief "message" + and the "details" parameters from the point of view of verbosity. + + It's meant to be used in cases where the executed command's standard error + is helpful enough to the user to be worth the extra text output. + """ + + def __init__( + self, message: str, *, stderr: Optional[Union[str, bytes]], **kwargs: Any + ) -> None: + super().__init__(message, **kwargs) + self._stderr = stderr + + @property + def stderr(self) -> Optional[str]: + if isinstance(self._stderr, bytes): + return self._stderr.decode("utf8", errors="replace") + # pyright needs the cast here + return cast(Optional[str], self._stderr) # type: ignore[redundant-cast] + + def __eq__(self, other: object) -> bool: + if isinstance(other, CraftCommandError): + return self._stderr == other._stderr and super().__eq__(other) + return NotImplemented + + class ArgumentParsingError(Exception): """Exception used when an argument parsing error is found.""" diff --git a/craft_cli/messages.py b/craft_cli/messages.py index 7574f06..8c1f9b2 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -45,6 +45,7 @@ except ImportError: _WINDOWS_MODE = False +from craft_cli import errors from craft_cli.printer import Printer if TYPE_CHECKING: @@ -52,8 +53,6 @@ from typing_extensions import Self - from craft_cli import errors - EmitterMode = enum.Enum("EmitterMode", "QUIET BRIEF VERBOSE DEBUG TRACE") """The different modes the Emitter can be set.""" @@ -723,6 +722,12 @@ def _report_error(self, error: errors.CraftError) -> None: # the initial message self._printer.show(sys.stderr, str(error), use_timestamp=use_timestamp, end_line=True) + if isinstance(error, errors.CraftCommandError): + stderr = error.stderr + if stderr: + text = f"Captured error:\n{stderr}" + self._printer.show(sys.stderr, text, use_timestamp=use_timestamp, end_line=True) + # detailed information and/or original exception if error.details: text = f"Detailed information: {error.details}" diff --git a/tests/unit/test_errors.py b/tests/unit/test_errors.py index b7f4d16..6042bf7 100644 --- a/tests/unit/test_errors.py +++ b/tests/unit/test_errors.py @@ -18,7 +18,7 @@ import pytest -from craft_cli.errors import CraftError +from craft_cli.errors import CraftError, CraftCommandError def test_crafterror_is_comparable(): @@ -78,3 +78,30 @@ def test_compare_crafterror_with_identical_attribute_values(argument_name): setattr(error2, argument_name, "foo") assert error1 == error2 + + +@pytest.mark.parametrize( + ("stderr", "expected"), [(None, None), ("text", "text"), (b"text", "text")] +) +def test_command_error(stderr, expected): + err = CraftCommandError("message", stderr=stderr) + assert err.stderr == expected + + +@pytest.mark.parametrize( + ("stderr1", "stderr2", "expected"), + [ + (None, None, True), + ("text", "text", True), + (b"text", b"text", True), + (None, "text", False), + (None, b"text", False), + (b"text", "text", False), + ], +) +def test_compare_command_error(stderr1, stderr2, expected): + err1 = CraftCommandError("message", stderr=stderr1) + err2 = CraftCommandError("message", stderr=stderr2) + + eq = err1 == err2 + assert eq == expected diff --git a/tests/unit/test_messages_emitter.py b/tests/unit/test_messages_emitter.py index c976447..e436062 100644 --- a/tests/unit/test_messages_emitter.py +++ b/tests/unit/test_messages_emitter.py @@ -23,7 +23,7 @@ import pytest from craft_cli import messages -from craft_cli.errors import CraftError +from craft_cli.errors import CraftError, CraftCommandError from craft_cli.messages import Emitter, EmitterMode, _Handler FAKE_LOG_NAME = "fakelog.log" @@ -1108,3 +1108,35 @@ def test_reporterror_both_url_and_slug(get_initiated_emitter): call().show(sys.stderr, full_docs_message, use_timestamp=False, end_line=True), call().stop(), ] + + +def test_reporterror_command_error(get_initiated_emitter): + stderr = b":: an error occurred\n:: on this line ^^\n" + error = CraftCommandError("test message", stderr=stderr, logpath_report=False) + + emitter = get_initiated_emitter(EmitterMode.BRIEF) + emitter.error(error) + + expected = "Captured error:\n:: an error occurred\n:: on this line ^^\n" + + assert emitter.printer_calls == [ + call().show(sys.stderr, "test message", use_timestamp=False, end_line=True), + call().show(sys.stderr, expected, use_timestamp=False, end_line=True), + call().stop(), + ] + + +@pytest.mark.parametrize("stderr", [None, "", b""]) +def test_reporterror_command_error_no_stderr(get_initiated_emitter, stderr): + error = CraftCommandError("test message", stderr=stderr, logpath_report=False) + + emitter = get_initiated_emitter(EmitterMode.BRIEF) + emitter.error(error) + + expected = "Captured error:\n:: an error occurred\n:: on this line ^^\n" + + # No "Captured error (...)" output + assert emitter.printer_calls == [ + call().show(sys.stderr, "test message", use_timestamp=False, end_line=True), + call().stop(), + ] From aabf2a9c6e909fe8ea9687de6f5b51c0ffee64d2 Mon Sep 17 00:00:00 2001 From: Tiago Nobrega Date: Fri, 26 Jul 2024 11:40:30 -0300 Subject: [PATCH 34/35] chore: cleanup lints from new ruff --- craft_cli/dispatcher.py | 2 +- craft_cli/messages.py | 2 +- craft_cli/printer.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/craft_cli/dispatcher.py b/craft_cli/dispatcher.py index b38bb35..37fb701 100644 --- a/craft_cli/dispatcher.py +++ b/craft_cli/dispatcher.py @@ -211,7 +211,7 @@ class Dispatcher: :param default_command: the command to run if none was specified in the command line """ - def __init__( # noqa: PLR0913 (too many arguments) + def __init__( self, appname: str, commands_groups: list[CommandGroup], diff --git a/craft_cli/messages.py b/craft_cli/messages.py index 8c1f9b2..c873b96 100644 --- a/craft_cli/messages.py +++ b/craft_cli/messages.py @@ -304,7 +304,7 @@ def stop(self) -> None: class _StreamContextManager: """A context manager that provides a pipe for subprocess to write its output.""" - def __init__( # noqa: PLR0913 (too many arguments) + def __init__( self, printer: Printer, text: str | None, diff --git a/craft_cli/printer.py b/craft_cli/printer.py index c18e4c4..b93fa50 100644 --- a/craft_cli/printer.py +++ b/craft_cli/printer.py @@ -396,7 +396,7 @@ def show( # noqa: PLR0913 (too many parameters) if not avoid_logging: self._log(msg) - def progress_bar( # noqa: PLR0913 + def progress_bar( self, stream: TextIO | None, text: str, From 385785dff1e5fc1bad6c6f6cb5d51613007253ea Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Thu, 15 Aug 2024 08:53:38 -0400 Subject: [PATCH 35/35] ci: update renovate from starbase (#268) * ci: update renovate from starbase * chore: more * chore: upstream fixes --- .github/renovate.json5 | 110 ++++++++++++++++++++------ .github/workflows/check-renovate.yaml | 40 ++++++++++ 2 files changed, 124 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/check-renovate.yaml diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 09d90fa..8342105 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,16 +1,38 @@ { // Configuration file for RenovateBot: https://docs.renovatebot.com/configuration-options - extends: ["config:base"], + extends: ["config:recommended", ":semanticCommitTypeAll(build)"], labels: ["dependencies"], // For convenient searching in GitHub + baseBranches: ["$default", "/^hotfix\\/.*/"], pip_requirements: { - fileMatch: ["^tox.ini$", "(^|/)requirements([\\w-]*)\\.txt$"] + fileMatch: ["^tox.ini$", "(^|/)requirements([\\w-]*)\\.txt$", "^.pre-commit-config.yaml$"] }, packageRules: [ + { + // Internal package minor patch updates get top priority, with auto-merging + groupName: "internal package minor releases", + matchPackagePatterns: ["^craft-.*"], + matchUpdateTypes: ["minor", "patch", "pin", "digest"], + prPriority: 10, + automerge: true, + minimumReleaseAge: "0 seconds", + schedule: ["at any time"], + matchBaseBranches: ["$default"], // Only do minor releases on main + }, + { + // Same as above, but for hotfix branches, only for patch, and without auto-merging. + groupName: "internal package patch releases (hotfix)", + matchPackagePatterns: ["^craft-.*"], + matchUpdateTypes: ["patch", "pin", "digest"], + prPriority: 10, + minimumReleaseAge: "0 seconds", + schedule: ["at any time"], + matchBaseBranches: ["/^hotfix\\/.*/"], // All hotfix branches + }, { // Automerge patches, pin changes and digest changes. // Also groups these changes together. groupName: "bugfixes", - excludePackagePrefixes: ["dev", "lint", "types"], + excludeDepPatterns: ["lint/.*", "types/.*"], matchUpdateTypes: ["patch", "pin", "digest"], prPriority: 3, // Patches should go first! automerge: true @@ -18,28 +40,55 @@ { // Update all internal packages in one higher-priority PR groupName: "internal packages", - matchPackagePrefixes: ["craft-", "snap-"], - matchLanguages: ["python"], - prPriority: 2 + matchDepPatterns: ["craft-.*", "snap-.*"], + matchCategories: ["python"], + prPriority: 2, + matchBaseBranches: ["$default"], // Not for hotfix branches }, { - // GitHub Actions are higher priority to update than most dependencies. + // GitHub Actions are higher priority to update than most dependencies since they don't tend to break things. groupName: "GitHub Actions", matchManagers: ["github-actions"], prPriority: 1, automerge: true, }, // Everything not in one of these rules gets priority 0 and falls here. + { + //Do all pydantic-related updates together + groupName: "pydantic etc.", + matchPackagePatterns: ["^pydantic"], + }, { // Minor changes can be grouped and automerged for dev dependencies, but are also deprioritised. groupName: "development dependencies (non-major)", groupSlug: "dev-dependencies", - matchPackagePrefixes: [ - "dev", - "lint", - "types" + matchDepPatterns: [ + "dev/.*", + "lint/.*", + "types/.*" + ], + matchPackagePatterns: [ + // Brought from charmcraft. May not be complete. + // This helps group dependencies in requirements-dev.txt files. + "^(.*/)?autoflake$", + "^(.*/)?black$", + "^(.*/)?codespell$", + "^(.*/)?coverage$", + "^(.*/)?flake8$", + "^(.*/)?hypothesis$", + "^(.*/)?mypy$", + "^(.*/)?pycodestyle$", + "^(.*/)?docstyle$", + "^(.*/)?pyfakefs$", + "^(.*/)?pyflakes$", + "^(.*/)?pylint$", + "^(.*/)?pytest", + "^(.*/)?responses$", + "^(.*/)?ruff$", + "^(.*/)?twine$", + "^(.*/)?tox$", + "^(.*/)?types-", ], - excludePackagePatterns: ["ruff"], matchUpdateTypes: ["minor", "patch", "pin", "digest"], prPriority: -1, automerge: true @@ -48,14 +97,16 @@ // Documentation related updates groupName: "documentation dependencies", groupSlug: "doc-dependencies", - matchPackageNames: ["Sphinx"], - matchPackagePatterns: ["^[Ss]phinx.*$", "^furo$"], - matchPackagePrefixes: ["docs"], + matchPackageNames: ["Sphinx", "furo"], + matchPackagePatterns: ["[Ss]phinx.*$"], + matchDepPatterns: ["docs/.*"], + matchBaseBranches: ["$default"], // Not for hotfix branches }, { // Other major dependencies get deprioritised below minor dev dependencies. matchUpdateTypes: ["major"], - prPriority: -2 + prPriority: -2, + matchBaseBranches: ["$default"], // Not for hotfix branches }, { // Major dev dependencies are stone last, but grouped. @@ -63,19 +114,22 @@ groupSlug: "dev-dependencies", matchDepTypes: ["devDependencies"], matchUpdateTypes: ["major"], - prPriority: -3 + prPriority: -3, + matchBaseBranches: ["$default"], // Not for hotfix branches }, { - // Ruff is still unstable, so update it separately. - groupName: "ruff", - matchPackagePatterns: ["^(lint/)?ruff$"], - prPriority: -3 + // Pyright makes regular breaking changes in patch releases, so we separate these + // and do them independently. + matchPackageNames: ["pyright", "types/pyright"], + prPriority: -4, + matchBaseBranches: ["$default"], // Not for hotfix branches } ], - regexManagers: [ + customManagers: [ { // tox.ini can get updates too if we specify for each package. fileMatch: ["tox.ini"], + customType: "regex", depTypeTemplate: "devDependencies", matchStrings: [ "# renovate: datasource=(?\\S+)\n\\s+(?.*?)(\\[[\\w]*\\])*[=><]=?(?.*?)\n" @@ -84,18 +138,22 @@ { // .pre-commit-config.yaml version updates fileMatch: [".pre-commit-config.yaml"], - depTypeTemplate: "devDependencies", + customType: "regex", + datasourceTemplate: "pypi", + depTypeTemplate: "lint", matchStrings: [ - "# renovate: datasource=(?\\S+);\\s*depName=(?.*?)\n\s+rev: \"v?(?.*?)\"" + "- repo: .*/<(?\\S+)\\s*\\n\\s*rev:\s+\"?v?(?\\S*)\"?", ] } ], timezone: "Etc/UTC", - automergeSchedule: ["every weekend"], schedule: ["every weekend"], prConcurrentLimit: 2, // No more than 2 open PRs at a time. + branchConcurrentLimit: 20, // No more than 20 open branches at a time. prCreation: "not-pending", // Wait until status checks have completed before raising the PR prNotPendingHours: 4, // ...unless the status checks have been running for 4+ hours. prHourlyLimit: 1, // No more than 1 PR per hour. - stabilityDays: 2 // Wait 2 days from release before updating. + minimumReleaseAge: "2 days", + automergeStrategy: "squash", // Squash & rebase when auto-merging. + semanticCommitType: "build" // use `build` as commit header type (i.e. `build(deps): `) } diff --git a/.github/workflows/check-renovate.yaml b/.github/workflows/check-renovate.yaml new file mode 100644 index 0000000..7d13cfb --- /dev/null +++ b/.github/workflows/check-renovate.yaml @@ -0,0 +1,40 @@ +name: Renovate check +on: + pull_request: + paths: + - ".github/workflows/check-renovate.yaml" + - ".github/renovate.json5" + + # Allows triggering the workflow manually from the Actions tab + workflow_dispatch: + inputs: + enable_ssh_access: + type: boolean + description: 'Enable ssh access' + required: false + default: false + +jobs: + renovate: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Install renovate + run: npm install --global renovate + - name: Enable ssh access + uses: mxschmitt/action-tmate@v3 + if: ${{ inputs.enable_ssh_access }} + with: + limit-access-to-actor: true + - name: Check renovate config + run: renovate-config-validator .github/renovate.json5 + - name: Renovate dry-run + run: renovate --dry-run --autodiscover + env: + RENOVATE_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RENOVATE_USE_BASE_BRANCH_CONFIG: ${{ github.ref }}