diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 899f8fabf..f9c6df8fb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,9 +22,9 @@ jobs: sphinx: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: Set up Python - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v7.0.0 with: python-version: "3.14" - name: Install dependencies @@ -37,9 +37,9 @@ jobs: twine-check: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: Set up Python - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v7.0.0 with: python-version: "3.14" - name: Install dependencies diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7a0dd38dc..03e063bdd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,10 +22,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 with: fetch-depth: 0 - - uses: actions/setup-python@v6.2.0 + - uses: actions/setup-python@v7.0.0 with: python-version: "3.14" - run: pip install --upgrade tox @@ -41,3 +41,5 @@ jobs: run: tox -e isort -- --check - name: Run pylint Python code static checker (https://github.com/PyCQA/pylint) run: tox -e pylint + - name: Run pre-commit rst checks (https://pre-commit.com/) + run: tox -e pre-commit-rst diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 809c36a87..77a83b6a8 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -15,6 +15,6 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v6.0.1 + - uses: dessant/lock-threads@v6.0.2 with: process-only: 'issues' diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index 993606750..28e908522 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -29,8 +29,8 @@ jobs: pre_commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6.0.2 - - uses: actions/setup-python@v6.2.0 + - uses: actions/checkout@v7.0.0 + - uses: actions/setup-python@v7.0.0 with: python-version: "3.14" - name: install tox diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01829b244..3d8cbd704 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,14 +14,14 @@ jobs: id-token: write environment: pypi.org steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 with: fetch-depth: 0 token: ${{ secrets.RELEASE_GITHUB_TOKEN }} - name: Python Semantic Release id: release - uses: python-semantic-release/python-semantic-release@v10.5.3 + uses: python-semantic-release/python-semantic-release@v10.6.1 with: github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} @@ -32,7 +32,7 @@ jobs: if: steps.release.outputs.released == 'true' - name: Publish package distributions to GitHub Releases - uses: python-semantic-release/publish-action@v10.5.3 + uses: python-semantic-release/publish-action@v10.6.1 if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.RELEASE_GITHUB_TOKEN }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8b03ec138..d219ed23d 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v10.3.0 + - uses: actions/stale@v10.4.0 with: operations-per-run: 500 stale-issue-label: "stale" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4cd761ad6..fdbd70d20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,9 +46,9 @@ jobs: version: "3.14" toxenv: py314,smoke steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: Set up Python ${{ matrix.python.version }} - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v7.0.0 with: python-version: ${{ matrix.python.version }} - name: Install dependencies @@ -65,9 +65,9 @@ jobs: matrix: toxenv: [api_func_v4, cli_func_v4] steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: Set up Python - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v7.0.0 with: python-version: "3.14" - name: Install dependencies @@ -77,7 +77,7 @@ jobs: TOXENV: ${{ matrix.toxenv }} run: tox -- --override-ini='log_cli=True' - name: Upload codecov coverage - uses: codecov/codecov-action@v6.0.1 + uses: codecov/codecov-action@v7.0.0 with: files: ./coverage.xml flags: ${{ matrix.toxenv }} @@ -87,9 +87,9 @@ jobs: coverage: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v7.0.0 with: python-version: "3.14" - name: Install dependencies @@ -100,7 +100,7 @@ jobs: TOXENV: cover run: tox - name: Upload codecov coverage - uses: codecov/codecov-action@v6.0.1 + uses: codecov/codecov-action@v7.0.0 with: files: ./coverage.xml flags: unit @@ -111,8 +111,8 @@ jobs: runs-on: ubuntu-latest name: Python wheel steps: - - uses: actions/checkout@v6.0.2 - - uses: actions/setup-python@v6.2.0 + - uses: actions/checkout@v7.0.0 + - uses: actions/setup-python@v7.0.0 with: python-version: "3.14" - name: Install dependencies @@ -129,9 +129,9 @@ jobs: runs-on: ubuntu-latest needs: [dist] steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@v7.0.0 - name: Set up Python - uses: actions/setup-python@v6.2.0 + uses: actions/setup-python@v7.0.0 with: python-version: '3.14' - uses: actions/download-artifact@v8.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0be8e1b2..289f80a00 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: hooks: - id: black - repo: https://github.com/commitizen-tools/commitizen - rev: v4.16.2 + rev: v4.16.4 hooks: - id: commitizen stages: [commit-msg] @@ -20,7 +20,7 @@ repos: hooks: - id: isort - repo: https://github.com/pycqa/pylint - rev: v4.0.5 + rev: v4.0.6 hooks: - id: pylint additional_dependencies: @@ -32,7 +32,7 @@ repos: - requests-toolbelt==1.0.0 files: 'gitlab/' - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.20.2 + rev: v2.3.0 hooks: - id: mypy args: [] @@ -51,6 +51,6 @@ repos: - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/maxbrunet/pre-commit-renovate - rev: 43.150.0 + rev: 43.261.4 hooks: - id: renovate-config-validator diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b012cf6..13262ec35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,52 @@ All versions below are listed in reverse chronological order. +## v8.5.0 (2026-07-28) + +### Bug Fixes + +- Typo + ([`72134f5`](https://github.com/python-gitlab/python-gitlab/commit/72134f5369cd4568a2374f25543071245375d26e)) + +- **api**: Allow ProjectMergeRequestDiscussionNote.resolved to be updated + ([`b411e5a`](https://github.com/python-gitlab/python-gitlab/commit/b411e5a20a4370a7c1b6968c85251b57785a9d03)) + +- **client**: Retry fetching server version after an unknown result + ([`046fbac`](https://github.com/python-gitlab/python-gitlab/commit/046fbac664e033a0a5a65417bfd229e91a1fd09f)) + +### Continuous Integration + +- Run the pre-commit rst checks in the CI + ([`a9c5d17`](https://github.com/python-gitlab/python-gitlab/commit/a9c5d17d3ab57d448be6dd5eac3b7ec9eb680261)) + +### Documentation + +- Correct error with backticks + ([`e57559e`](https://github.com/python-gitlab/python-gitlab/commit/e57559e8c1bea1ba580074e83d21072d9b80d582)) + +- **discussions**: Add example of resolving a single project merge request discussion note + ([`2ef6452`](https://github.com/python-gitlab/python-gitlab/commit/2ef6452b9471c1a421ac8addc06eb3b5052289e0)) + +### Features + +- **api**: Add ListMixin to remaining *DiscussionNoteManagers + ([`0e2347c`](https://github.com/python-gitlab/python-gitlab/commit/0e2347cc181214bd3d825eac3cc94239e6cd2ef5)) + +- **client**: Additionally handle retries for request timeouts + ([`4e32439`](https://github.com/python-gitlab/python-gitlab/commit/4e32439deaa9516a48becf9a7fc9cdb7723207c6)) + +- **hooks**: Support custom webhook template on project hooks + ([`39dc990`](https://github.com/python-gitlab/python-gitlab/commit/39dc9902af1383ea5f4b5efb8c2e42ab71f2910f)) + +- **users**: Allow updating only a user's email, username or name + ([`0d2afb8`](https://github.com/python-gitlab/python-gitlab/commit/0d2afb878ea28038b229c7a14ddfdbe393f22e18)) + +### Testing + +- **api**: Ensure ProjectMergeRequestDiscussionNote.resolved can be updated successfully + ([`57fd60c`](https://github.com/python-gitlab/python-gitlab/commit/57fd60c1c606b98b43ca3dcde48b1fa841635e76)) + + ## v8.4.0 (2026-05-28) ### Features diff --git a/docs/api-usage-advanced.rst b/docs/api-usage-advanced.rst index d6514c7b3..06c060455 100644 --- a/docs/api-usage-advanced.rst +++ b/docs/api-usage-advanced.rst @@ -147,7 +147,8 @@ python-gitlab can automatically retry in such case, when ``retry_transient_errors`` argument is set to ``True``. When enabled, HTTP error codes 500 (Internal Server Error), 502 (502 Bad Gateway), 503 (Service Unavailable), 504 (Gateway Timeout), and Cloudflare -errors (520-530) are retried. +errors (520-530) are retried. ``requests.exceptions.Timeout`` are also +handled. Additionally, HTTP error code 409 (Conflict) is retried if the reason is a diff --git a/docs/gl_objects/discussions.rst b/docs/gl_objects/discussions.rst index f64a98b3d..24f955b9f 100644 --- a/docs/gl_objects/discussions.rst +++ b/docs/gl_objects/discussions.rst @@ -75,6 +75,12 @@ You can get and update a single note using the ``*DiscussionNote`` resources:: last_note.body = 'Updated comment' last_note.save() +Resolve / unresolve a note:: + + last_note = discussion.notes.get(note_id) + last_note.resolved = True # True to resolve, False to unresolve + last_note.save() + Create a new discussion:: discussion = resource.discussions.create({'body': 'First comment of discussion'}) diff --git a/gitlab/_version.py b/gitlab/_version.py index 57b49906f..e634f388a 100644 --- a/gitlab/_version.py +++ b/gitlab/_version.py @@ -3,4 +3,4 @@ __email__ = "gauvainpocentek@gmail.com" __license__ = "LGPL3" __title__ = "python-gitlab" -__version__ = "8.4.0" +__version__ = "8.5.0" diff --git a/gitlab/client.py b/gitlab/client.py index ea3a0c209..f6fe89d84 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -60,7 +60,7 @@ class Gitlab: order_by: Set order_by globally user_agent: A custom user agent to use for making HTTP requests. retry_transient_errors: Whether to retry after 500, 502, 503, 504 - or 52x responses. Defaults to False. + or 52x responses, or after a request timeout. Defaults to False. keep_base_url: keep user-provided base URL for pagination if it differs from response headers @@ -404,7 +404,7 @@ def version(self) -> tuple[str, str]: The server version and server revision. ('unknown', 'unknown') if the server doesn't perform as expected. """ - if self._server_version is None: + if self._server_version in (None, "unknown"): try: data = self.http_get("/version") if isinstance(data, dict): @@ -665,7 +665,7 @@ def http_request( obey_rate_limit: Whether to obey 429 Too Many Request responses. Defaults to True. retry_transient_errors: Whether to retry after 500, 502, 503, 504 - or 52x responses. Defaults to False. + or 52x responses, or after a request timeout. Defaults to False. max_retries: Max retries after 429 or transient errors, set to -1 to retry forever. Defaults to 10. extra_headers: Add and override HTTP headers for the request. @@ -737,7 +737,11 @@ def http_request( stream=streamed, **opts, ) - except (requests.ConnectionError, requests.exceptions.ChunkedEncodingError): + except ( + requests.ConnectionError, + requests.exceptions.ChunkedEncodingError, + requests.exceptions.Timeout, + ): if retry.handle_retry(): continue raise @@ -793,7 +797,7 @@ def http_get( **kwargs: Extra options to send to the server (e.g. sudo) Returns: - A requests result object is streamed is True or the content type is + A requests result object if streamed is True or the content type is not json. The parsed json data otherwise. diff --git a/gitlab/v4/objects/hooks.py b/gitlab/v4/objects/hooks.py index f9ce553bb..2a4de1017 100644 --- a/gitlab/v4/objects/hooks.py +++ b/gitlab/v4/objects/hooks.py @@ -60,6 +60,7 @@ class ProjectHookManager(CRUDMixin[ProjectHook]): "wiki_page_events", "enable_ssl_verification", "token", + "custom_webhook_template", ), ) _update_attrs = RequiredOptional( @@ -76,6 +77,7 @@ class ProjectHookManager(CRUDMixin[ProjectHook]): "wiki_events", "enable_ssl_verification", "token", + "custom_webhook_template", ), ) diff --git a/gitlab/v4/objects/notes.py b/gitlab/v4/objects/notes.py index 3e83d9be1..f838923e7 100644 --- a/gitlab/v4/objects/notes.py +++ b/gitlab/v4/objects/notes.py @@ -1,14 +1,5 @@ from gitlab.base import RESTObject -from gitlab.mixins import ( - CreateMixin, - CRUDMixin, - DeleteMixin, - GetMixin, - ObjectDeleteMixin, - RetrieveMixin, - SaveMixin, - UpdateMixin, -) +from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, RetrieveMixin, SaveMixin from gitlab.types import RequiredOptional from .award_emojis import ( # noqa: F401 @@ -58,12 +49,7 @@ class GroupEpicDiscussionNote(SaveMixin, ObjectDeleteMixin, RESTObject): pass -class GroupEpicDiscussionNoteManager( - GetMixin[GroupEpicDiscussionNote], - CreateMixin[GroupEpicDiscussionNote], - UpdateMixin[GroupEpicDiscussionNote], - DeleteMixin[GroupEpicDiscussionNote], -): +class GroupEpicDiscussionNoteManager(CRUDMixin[GroupEpicDiscussionNote]): _path = "/groups/{group_id}/epics/{epic_id}/discussions/{discussion_id}/notes" _obj_cls = GroupEpicDiscussionNote _from_parent_attrs = { @@ -90,12 +76,7 @@ class ProjectCommitDiscussionNote(SaveMixin, ObjectDeleteMixin, RESTObject): pass -class ProjectCommitDiscussionNoteManager( - GetMixin[ProjectCommitDiscussionNote], - CreateMixin[ProjectCommitDiscussionNote], - UpdateMixin[ProjectCommitDiscussionNote], - DeleteMixin[ProjectCommitDiscussionNote], -): +class ProjectCommitDiscussionNoteManager(CRUDMixin[ProjectCommitDiscussionNote]): _path = ( "/projects/{project_id}/repository/commits/{commit_id}/" "discussions/{discussion_id}/notes" @@ -172,7 +153,7 @@ class ProjectMergeRequestDiscussionNoteManager( "discussion_id": "id", } _create_attrs = RequiredOptional(required=("body",), optional=("created_at",)) - _update_attrs = RequiredOptional(required=("body",)) + _update_attrs = RequiredOptional(exclusive=("body", "resolved")) class ProjectSnippetNote(SaveMixin, ObjectDeleteMixin, RESTObject): @@ -191,12 +172,7 @@ class ProjectSnippetDiscussionNote(SaveMixin, ObjectDeleteMixin, RESTObject): pass -class ProjectSnippetDiscussionNoteManager( - GetMixin[ProjectSnippetDiscussionNote], - CreateMixin[ProjectSnippetDiscussionNote], - UpdateMixin[ProjectSnippetDiscussionNote], - DeleteMixin[ProjectSnippetDiscussionNote], -): +class ProjectSnippetDiscussionNoteManager(CRUDMixin[ProjectSnippetDiscussionNote]): _path = ( "/projects/{project_id}/snippets/{snippet_id}/" "discussions/{discussion_id}/notes" diff --git a/gitlab/v4/objects/users.py b/gitlab/v4/objects/users.py index be0e36529..3978b8aba 100644 --- a/gitlab/v4/objects/users.py +++ b/gitlab/v4/objects/users.py @@ -453,8 +453,10 @@ class UserManager(CRUDMixin[User]): ) ) _update_attrs = RequiredOptional( - required=("email", "username", "name"), optional=( + "email", + "username", + "name", "password", "skype", "linkedin", @@ -475,7 +477,7 @@ class UserManager(CRUDMixin[User]): "private_profile", "color_scheme_id", "theme_id", - ), + ) ) _types = {"confirm": types.LowercaseStringAttribute, "avatar": types.ImageAttribute} diff --git a/pyproject.toml b/pyproject.toml index 45e8c36f8..511e6c366 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ license = {text = "LGPL-3.0-or-later"} dynamic = ["version"] [project.optional-dependencies] -autocompletion = ["argcomplete>=1.10.0,<3"] +autocompletion = ["argcomplete>=1.10.0,<4"] yaml = ["PyYaml>=6.0.1"] graphql = ["gql[httpx]>=3.5.0,<5"] diff --git a/requirements-lint.txt b/requirements-lint.txt index 9fefc4ee7..195b8b1c6 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,14 +1,14 @@ -r requirements.txt argcomplete==2.0.0 black==26.5.1 -commitizen==4.16.2 +commitizen==4.16.4 flake8==7.3.0 isort==8.0.1 -mypy==1.20.2 -pylint==4.0.5 -pytest==9.0.3 -responses==0.26.1 +mypy==2.3.0 +pylint==4.0.6 +pytest==9.1.1 +responses==0.26.2 respx==0.23.1 types-PyYAML==6.0.12.20260518 -types-requests==2.33.0.20260518 +types-requests==2.33.0.20260712 types-setuptools==82.0.0.20260518 diff --git a/requirements-test.txt b/requirements-test.txt index 185116805..856c399ad 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,13 +1,13 @@ -r requirements.txt -anyio==4.13.0 -build==1.5.0 -coverage==7.14.0 +anyio==4.14.2 +build==1.5.1 +coverage==7.15.1 pytest-console-scripts==1.4.1 pytest-cov==7.1.0 -pytest-github-actions-annotate-failures==0.4.0 -pytest==9.0.3 +pytest-github-actions-annotate-failures==0.4.2 +pytest==9.1.1 PyYaml==6.0.3 -responses==0.26.1 +responses==0.26.2 respx==0.23.1 trio==0.33.0 wheel==0.47.0 diff --git a/tests/functional/api/test_merge_requests.py b/tests/functional/api/test_merge_requests.py index 8357a817d..f7a56721b 100644 --- a/tests/functional/api/test_merge_requests.py +++ b/tests/functional/api/test_merge_requests.py @@ -69,6 +69,13 @@ def test_merge_request_discussion(project): discussion = mr.discussions.get(discussion.id) assert discussion.attributes["notes"][-1]["body"] == "updated body" + note_from_get = discussion.notes.get(note.id) + note_from_get.resolved = True + note_from_get.save() + + discussion = mr.discussions.get(discussion.id) + assert discussion.attributes["notes"][-1]["resolved"] is True + note_from_get.delete() diff --git a/tests/unit/test_gitlab_http_methods.py b/tests/unit/test_gitlab_http_methods.py index f85035fc2..00d96d0be 100644 --- a/tests/unit/test_gitlab_http_methods.py +++ b/tests/unit/test_gitlab_http_methods.py @@ -146,6 +146,7 @@ def test_http_request_extra_headers(gl): [ requests.ConnectionError("Connection aborted."), requests.exceptions.ChunkedEncodingError("Connection broken."), + requests.exceptions.Timeout("Request timed out."), ], ) def test_http_request_with_retry_on_method_for_transient_network_failures( diff --git a/tox.ini b/tox.ini index 0ba295692..5e7c01076 100644 --- a/tox.ini +++ b/tox.ini @@ -146,9 +146,17 @@ commands = pytest tests/smoke {posargs} [testenv:pre-commit] skip_install = true -deps = -r requirements-precommit.txt +deps = -r {toxinidir}/requirements-precommit.txt commands = pre-commit run --all-files --show-diff-on-failure +[testenv:pre-commit-rst] +skip_install = true +deps = -r {toxinidir}/requirements-precommit.txt +commands = + pre-commit run rst-backticks --all-files --show-diff-on-failure + pre-commit run rst-directive-colons --all-files --show-diff-on-failure + pre-commit run rst-inline-touching-normal --all-files --show-diff-on-failure + [testenv:install] skip_install = true deps = -r{toxinidir}/requirements.txt