Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-python-gitlab for
openSUSE:Factory checked in at 2026-03-30 18:31:32
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-python-gitlab (Old)
and /work/SRC/openSUSE:Factory/.python-python-gitlab.new.1999 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-python-gitlab"
Mon Mar 30 18:31:32 2026 rev:31 rq:1343535 version:8.2.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-python-gitlab/python-python-gitlab.changes
2026-03-02 17:41:30.066265083 +0100
+++
/work/SRC/openSUSE:Factory/.python-python-gitlab.new.1999/python-python-gitlab.changes
2026-03-30 18:34:52.280018471 +0200
@@ -1,0 +2,13 @@
+Sun Mar 29 18:21:31 UTC 2026 - Johannes Kastl
<[email protected]>
+
+- update to 8.2.0:
+ * Documentation
+ - testing: Document passing pytest options during local
+ development (e6669f9)
+ * Features
+ - api: Add support for project feature flags and feature flag
+ user lists (be68285)
+ - projects: Add optional parameter to set approval rule on all
+ protected branches. (8d76028)
+
+-------------------------------------------------------------------
Old:
----
python_gitlab-8.1.0.tar.gz
New:
----
python_gitlab-8.2.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-python-gitlab.spec ++++++
--- /var/tmp/diff_new_pack.TYkiaX/_old 2026-03-30 18:34:52.940046065 +0200
+++ /var/tmp/diff_new_pack.TYkiaX/_new 2026-03-30 18:34:52.944046232 +0200
@@ -17,7 +17,7 @@
Name: python-python-gitlab
-Version: 8.1.0
+Version: 8.2.0
Release: 0
Summary: Python module for interacting with the GitLab API
License: LGPL-3.0-only
++++++ python_gitlab-8.1.0.tar.gz -> python_gitlab-8.2.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/CHANGELOG.md
new/python_gitlab-8.2.0/CHANGELOG.md
--- old/python_gitlab-8.1.0/CHANGELOG.md 2026-02-28 02:26:08.000000000
+0100
+++ new/python_gitlab-8.2.0/CHANGELOG.md 2026-03-28 02:49:39.000000000
+0100
@@ -2,6 +2,22 @@
All versions below are listed in reverse chronological order.
+## v8.2.0 (2026-03-28)
+
+### Documentation
+
+- **testing**: Document passing pytest options during local development
+
([`e6669f9`](https://github.com/python-gitlab/python-gitlab/commit/e6669f96d662d310109afa4a61fe8dabbd780a4e))
+
+### Features
+
+- **api**: Add support for project feature flags and feature flag user lists
+
([`be68285`](https://github.com/python-gitlab/python-gitlab/commit/be68285793f35afc10a72b59da3fb24429631f54))
+
+- **projects**: Add optional parameter to set approval rule on all protected
branches.
+
([`8d76028`](https://github.com/python-gitlab/python-gitlab/commit/8d76028a1ae3554527291dc98e6be041ff089ec5))
+
+
## v8.1.0 (2026-02-28)
### Bug Fixes
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/PKG-INFO
new/python_gitlab-8.2.0/PKG-INFO
--- old/python_gitlab-8.1.0/PKG-INFO 2026-02-28 02:26:14.690398200 +0100
+++ new/python_gitlab-8.2.0/PKG-INFO 2026-03-28 02:49:46.099087200 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: python-gitlab
-Version: 8.1.0
+Version: 8.2.0
Summary: The python wrapper for the GitLab REST and GraphQL APIs.
Author-email: Gauvain Pocentek <[email protected]>
Maintainer-email: John Villalovos <[email protected]>, Max Wittig
<[email protected]>, Nejc Habjan <[email protected]>, Roger Meier
<[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/docs/api-objects.rst
new/python_gitlab-8.2.0/docs/api-objects.rst
--- old/python_gitlab-8.1.0/docs/api-objects.rst 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/docs/api-objects.rst 2026-03-28
02:49:29.000000000 +0100
@@ -24,7 +24,7 @@
gl_objects/environments
gl_objects/events
gl_objects/epics
- gl_objects/features
+ gl_objects/gitlab_features
gl_objects/geo_nodes
gl_objects/groups
gl_objects/group_access_tokens
@@ -49,6 +49,8 @@
gl_objects/pipelines_and_jobs
gl_objects/projects
gl_objects/project_access_tokens
+ gl_objects/project_feature_flags
+ gl_objects/project_feature_flag_user_lists
gl_objects/protected_branches
gl_objects/protected_container_repositories
gl_objects/protected_environments
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/docs/gl_objects/features.rst
new/python_gitlab-8.2.0/docs/gl_objects/features.rst
--- old/python_gitlab-8.1.0/docs/gl_objects/features.rst 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/docs/gl_objects/features.rst 1970-01-01
01:00:00.000000000 +0100
@@ -1,32 +0,0 @@
-##############
-Features flags
-##############
-
-Reference
----------
-
-* v4 API:
-
- + :class:`gitlab.v4.objects.Feature`
- + :class:`gitlab.v4.objects.FeatureManager`
- + :attr:`gitlab.Gitlab.features`
-
-* GitLab API: https://docs.gitlab.com/api/features
-
-Examples
---------
-
-List features::
-
- features = gl.features.list(get_all=True)
-
-Create or set a feature::
-
- feature = gl.features.set(feature_name, True)
- feature = gl.features.set(feature_name, 30)
- feature = gl.features.set(feature_name, True, user=filipowm)
- feature = gl.features.set(feature_name, 40, group=mygroup)
-
-Delete a feature::
-
- feature.delete()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/docs/gl_objects/gitlab_features.rst
new/python_gitlab-8.2.0/docs/gl_objects/gitlab_features.rst
--- old/python_gitlab-8.1.0/docs/gl_objects/gitlab_features.rst 1970-01-01
01:00:00.000000000 +0100
+++ new/python_gitlab-8.2.0/docs/gl_objects/gitlab_features.rst 2026-03-28
02:49:29.000000000 +0100
@@ -0,0 +1,37 @@
+################################
+GitLab Development Feature Flags
+################################
+
+.. note::
+
+ This API is for managing GitLab's internal development feature flags and
requires administrator access.
+ For project-level feature flags, see :doc:`project_feature_flags`.
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.Feature`
+ + :class:`gitlab.v4.objects.FeatureManager`
+ + :attr:`gitlab.Gitlab.features`
+
+* GitLab API: https://docs.gitlab.com/api/features
+
+Examples
+--------
+
+List features::
+
+ features = gl.features.list(get_all=True)
+
+Create or set a feature::
+
+ feature = gl.features.set(feature_name, True)
+ feature = gl.features.set(feature_name, 30)
+ feature = gl.features.set(feature_name, True, user=filipowm)
+ feature = gl.features.set(feature_name, 40, group=mygroup)
+
+Delete a feature::
+
+ feature.delete()
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/docs/gl_objects/project_feature_flag_user_lists.rst
new/python_gitlab-8.2.0/docs/gl_objects/project_feature_flag_user_lists.rst
--- old/python_gitlab-8.1.0/docs/gl_objects/project_feature_flag_user_lists.rst
1970-01-01 01:00:00.000000000 +0100
+++ new/python_gitlab-8.2.0/docs/gl_objects/project_feature_flag_user_lists.rst
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,51 @@
+###############################
+Project Feature Flag User Lists
+###############################
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.ProjectFeatureFlagUserList`
+ + :class:`gitlab.v4.objects.ProjectFeatureFlagUserListManager`
+ + :attr:`gitlab.v4.objects.Project.feature_flags_user_lists`
+
+* GitLab API: https://docs.gitlab.com/api/feature_flag_user_lists
+
+Examples
+--------
+
+List user lists::
+
+ user_lists = project.feature_flags_user_lists.list()
+
+Get a user list::
+
+ user_list = project.feature_flags_user_lists.get(list_iid)
+
+Create a user list::
+
+ user_list = project.feature_flags_user_lists.create({
+ 'name': 'my_user_list',
+ 'user_xids': 'user1,user2,user3'
+ })
+
+Update a user list::
+
+ user_list.name = 'updated_list_name'
+ user_list.user_xids = 'user1,user2'
+ user_list.save()
+
+Delete a user list::
+
+ user_list.delete()
+
+Search for a user list::
+
+ user_lists = project.feature_flags_user_lists.list(search='my_list')
+
+See also
+--------
+
+* :doc:`project_feature_flags`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/docs/gl_objects/project_feature_flags.rst
new/python_gitlab-8.2.0/docs/gl_objects/project_feature_flags.rst
--- old/python_gitlab-8.1.0/docs/gl_objects/project_feature_flags.rst
1970-01-01 01:00:00.000000000 +0100
+++ new/python_gitlab-8.2.0/docs/gl_objects/project_feature_flags.rst
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,63 @@
+#####################
+Project Feature Flags
+#####################
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.ProjectFeatureFlag`
+ + :class:`gitlab.v4.objects.ProjectFeatureFlagManager`
+ + :attr:`gitlab.v4.objects.Project.feature_flags`
+
+* GitLab API: https://docs.gitlab.com/api/feature_flags
+
+Examples
+--------
+
+List feature flags::
+
+ flags = project.feature_flags.list()
+
+Get a feature flag::
+
+ flag = project.feature_flags.get('my_feature_flag')
+
+Create a feature flag::
+
+ flag = project.feature_flags.create({'name': 'my_feature_flag', 'version':
'new_version_flag'})
+
+Create a feature flag with strategies::
+
+ flag = project.feature_flags.create({
+ 'name': 'my_complex_flag',
+ 'version': 'new_version_flag',
+ 'strategies': [{
+ 'name': 'userWithId',
+ 'parameters': {'userIds': 'user1,user2'}
+ }]
+ })
+
+Update a feature flag::
+
+ flag.description = 'Updated description'
+ flag.save()
+
+Rename a feature flag::
+
+ # You can rename a flag by changing its name attribute and calling save()
+ flag.name = 'new_flag_name'
+ flag.save()
+
+ # Alternatively, you can use the manager's update method
+ project.feature_flags.update('old_flag_name', {'name': 'new_flag_name'})
+
+Delete a feature flag::
+
+ flag.delete()
+
+See also
+--------
+
+* :doc:`project_feature_flag_user_lists`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/docs/gl_objects/projects.rst
new/python_gitlab-8.2.0/docs/gl_objects/projects.rst
--- old/python_gitlab-8.1.0/docs/gl_objects/projects.rst 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/docs/gl_objects/projects.rst 2026-03-28
02:49:29.000000000 +0100
@@ -409,6 +409,44 @@
project.customattributes.set('type', 'internal')
gl.projects.list(custom_attributes={'type': 'internal'}, get_all=True)
+Project feature flags
+=====================
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.ProjectFeatureFlag`
+ + :class:`gitlab.v4.objects.ProjectFeatureFlagManager`
+ + :attr:`gitlab.v4.objects.Project.feature_flags`
+
+* GitLab API: https://docs.gitlab.com/api/feature_flags
+
+Examples
+--------
+
+See :doc:`project_feature_flags`.
+
+Project feature flag user lists
+===============================
+
+Reference
+---------
+
+* v4 API:
+
+ + :class:`gitlab.v4.objects.ProjectFeatureFlagUserList`
+ + :class:`gitlab.v4.objects.ProjectFeatureFlagUserListManager`
+ + :attr:`gitlab.v4.objects.Project.feature_flags_user_lists`
+
+* GitLab API: https://docs.gitlab.com/api/feature_flag_user_lists
+
+Examples
+--------
+
+See :doc:`project_feature_flag_user_lists`.
+
Project files
=============
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/gitlab/_version.py
new/python_gitlab-8.2.0/gitlab/_version.py
--- old/python_gitlab-8.1.0/gitlab/_version.py 2026-02-28 02:26:08.000000000
+0100
+++ new/python_gitlab-8.2.0/gitlab/_version.py 2026-03-28 02:49:39.000000000
+0100
@@ -3,4 +3,4 @@
__email__ = "[email protected]"
__license__ = "LGPL3"
__title__ = "python-gitlab"
-__version__ = "8.1.0"
+__version__ = "8.2.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/gitlab/types.py
new/python_gitlab-8.2.0/gitlab/types.py
--- old/python_gitlab-8.1.0/gitlab/types.py 2026-02-28 02:25:57.000000000
+0100
+++ new/python_gitlab-8.2.0/gitlab/types.py 2026-03-28 02:49:29.000000000
+0100
@@ -1,8 +1,11 @@
from __future__ import annotations
import dataclasses
+import json
from typing import Any, TYPE_CHECKING
+from gitlab import exceptions
+
@dataclasses.dataclass(frozen=True)
class RequiredOptional:
@@ -36,6 +39,13 @@
class GitlabAttribute:
+ # Used in utils._transform_types() to decide if we should call
get_for_api()
+ # on the attribute when transform_data is False (e.g. for POST/PUT/PATCH).
+ #
+ # This allows us to force transformation of data even when sending JSON
bodies,
+ # which is useful for types like CommaSeparatedStringAttribute.
+ transform_in_body = False
+
def __init__(self, value: Any = None) -> None:
self._value = value
@@ -49,6 +59,16 @@
return (key, self._value)
+class JsonAttribute(GitlabAttribute):
+ def set_from_cli(self, cli_value: str) -> None:
+ try:
+ self._value = json.loads(cli_value)
+ except (ValueError, TypeError) as e:
+ raise exceptions.GitlabParsingError(
+ f"Could not parse JSON data: {e}"
+ ) from e
+
+
class _ListArrayAttribute(GitlabAttribute):
"""Helper class to support `list` / `array` types."""
@@ -82,9 +102,23 @@
class CommaSeparatedListAttribute(_ListArrayAttribute):
- """For values which are sent to the server as a Comma Separated Values
- (CSV) string. We allow them to be specified as a list and we convert it
- into a CSV"""
+ """
+ For values which are sent to the server as a Comma Separated Values (CSV)
string
+ in query parameters (GET), but as a list/array in JSON bodies (POST/PUT).
+ """
+
+
+class CommaSeparatedStringAttribute(_ListArrayAttribute):
+ """
+ For values which are sent to the server as a Comma Separated Values (CSV)
string.
+ Unlike CommaSeparatedListAttribute, this type ensures the value is
converted
+ to a string even in JSON bodies (POST/PUT requests).
+ """
+
+ # Used in utils._transform_types() to ensure the value is converted to a
string
+ # via get_for_api() even when transform_data is False (e.g. for
POST/PUT/PATCH).
+ # This is needed because some APIs require a CSV string instead of a JSON
array.
+ transform_in_body = True
class LowercaseStringAttribute(GitlabAttribute):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/gitlab/utils.py
new/python_gitlab-8.2.0/gitlab/utils.py
--- old/python_gitlab-8.1.0/gitlab/utils.py 2026-02-28 02:25:57.000000000
+0100
+++ new/python_gitlab-8.2.0/gitlab/utils.py 2026-03-28 02:49:29.000000000
+0100
@@ -198,7 +198,15 @@
files[attr_name] = (key, data.pop(attr_name))
continue
- if not transform_data:
+ # If transform_data is False, it means we are preparing data for a
JSON body
+ # (POST/PUT/PATCH). In this case, we normally skip transformation
because
+ # most types (like ArrayAttribute) only need transformation for query
+ # parameters (GET).
+ #
+ # However, some types (like CommaSeparatedStringAttribute) need to be
+ # transformed even in JSON bodies (e.g. converting a list to a CSV
string).
+ # The 'transform_in_body' flag on the attribute class controls this
behavior.
+ if not transform_data and not gitlab_attribute.transform_in_body:
continue
if isinstance(gitlab_attribute, types.GitlabAttribute):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/gitlab/v4/objects/__init__.py
new/python_gitlab-8.2.0/gitlab/v4/objects/__init__.py
--- old/python_gitlab-8.1.0/gitlab/v4/objects/__init__.py 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/gitlab/v4/objects/__init__.py 2026-03-28
02:49:29.000000000 +0100
@@ -24,6 +24,8 @@
from .epics import *
from .events import *
from .export_import import *
+from .feature_flag_user_lists import *
+from .feature_flags import *
from .features import *
from .files import *
from .geo_nodes import *
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/gitlab/v4/objects/feature_flag_user_lists.py
new/python_gitlab-8.2.0/gitlab/v4/objects/feature_flag_user_lists.py
--- old/python_gitlab-8.1.0/gitlab/v4/objects/feature_flag_user_lists.py
1970-01-01 01:00:00.000000000 +0100
+++ new/python_gitlab-8.2.0/gitlab/v4/objects/feature_flag_user_lists.py
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,27 @@
+"""
+GitLab API:
+https://docs.gitlab.com/api/feature_flag_user_lists
+"""
+
+from __future__ import annotations
+
+from gitlab import types
+from gitlab.base import RESTObject
+from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
+from gitlab.types import RequiredOptional
+
+__all__ = ["ProjectFeatureFlagUserList", "ProjectFeatureFlagUserListManager"]
+
+
+class ProjectFeatureFlagUserList(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _id_attr = "iid"
+
+
+class ProjectFeatureFlagUserListManager(CRUDMixin[ProjectFeatureFlagUserList]):
+ _path = "/projects/{project_id}/feature_flags_user_lists"
+ _obj_cls = ProjectFeatureFlagUserList
+ _from_parent_attrs = {"project_id": "id"}
+ _create_attrs = RequiredOptional(required=("name", "user_xids"))
+ _update_attrs = RequiredOptional(optional=("name", "user_xids"))
+ _list_filters = ("search",)
+ _types = {"user_xids": types.CommaSeparatedStringAttribute}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/gitlab/v4/objects/feature_flags.py
new/python_gitlab-8.2.0/gitlab/v4/objects/feature_flags.py
--- old/python_gitlab-8.1.0/gitlab/v4/objects/feature_flags.py 1970-01-01
01:00:00.000000000 +0100
+++ new/python_gitlab-8.2.0/gitlab/v4/objects/feature_flags.py 2026-03-28
02:49:29.000000000 +0100
@@ -0,0 +1,106 @@
+"""
+GitLab API:
+https://docs.gitlab.com/api/feature_flags
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+from gitlab import types, utils
+from gitlab.base import RESTObject
+from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
+from gitlab.types import RequiredOptional
+
+__all__ = ["ProjectFeatureFlag", "ProjectFeatureFlagManager"]
+
+
+class ProjectFeatureFlag(SaveMixin, ObjectDeleteMixin, RESTObject):
+ _id_attr = "name"
+ manager: ProjectFeatureFlagManager
+
+ def _get_save_url_id(self) -> str | int | None:
+ """Get the ID used to construct the API URL for the save operation.
+
+ For renames, this must be the *original* name of the flag. For other
+ updates, it is the current name.
+ """
+ if self._id_attr in self._updated_attrs:
+ # If the name is being changed, use the original name for the URL.
+ obj_id = self._attrs.get(self._id_attr)
+ if isinstance(obj_id, str):
+ return utils.EncodedId(obj_id)
+ return obj_id
+ return self.encoded_id
+
+ def save(self, **kwargs: Any) -> dict[str, Any] | None:
+ """Save the changes made to the object to the server.
+
+ This is the standard method to use when updating a feature flag object
+ that you have already retrieved.
+
+ It is overridden here to correctly handle renaming. When `name` is
+ changed, the API requires the *original* name in the URL, and this
+ method provides it.
+
+ Args:
+ **kwargs: Extra options to send to the server (e.g. sudo)
+
+ Returns:
+ The new object data (*not* a RESTObject)
+
+ Raises:
+ GitlabAuthenticationError: If authentication is not correct
+ GitlabUpdateError: If the server cannot perform the request
+ """
+ updated_data = self._get_updated_data()
+ if not updated_data:
+ return None
+
+ obj_id = self._get_save_url_id()
+ server_data = self.manager.update(obj_id, updated_data, **kwargs)
+ self._update_attrs(server_data)
+ return server_data
+
+
+class ProjectFeatureFlagManager(CRUDMixin[ProjectFeatureFlag]):
+ _path = "/projects/{project_id}/feature_flags"
+ _obj_cls = ProjectFeatureFlag
+ _from_parent_attrs = {"project_id": "id"}
+ _create_attrs = RequiredOptional(
+ required=("name",), optional=("version", "description", "active",
"strategies")
+ )
+ _update_attrs = RequiredOptional(
+ # new_name is used for renaming via CLI and mapped to 'name' in
update()
+ optional=("name", "new_name", "description", "active", "strategies")
+ )
+ _list_filters = ("scope",)
+ _types = {"strategies": types.JsonAttribute}
+
+ def update(
+ self,
+ id: str | int | None = None,
+ new_data: dict[str, Any] | None = None,
+ **kwargs: Any,
+ ) -> dict[str, Any]:
+ """Update a Project Feature Flag.
+
+ This is a lower-level method called by `ProjectFeatureFlag.save()` and
+ is also used directly by the CLI.
+
+ The `new_name` parameter is a special case to support renaming via the
+ CLI (`--new-name`). It is converted to the `name` parameter that the
+ GitLab API expects in the request body.
+
+ Args:
+ id: The current name of the feature flag.
+ new_data: The dictionary of attributes to update.
+ **kwargs: Extra options to send to the server (e.g. sudo)
+ """
+ # Avoid mutating the caller-provided new_data dict by working on a
copy.
+ data = dict(new_data or {})
+ # When used via CLI, we have 'new_name' to distinguish from the ID
'name'.
+ # When used via .save(), the object passes 'name' directly in new_data.
+ if "new_name" in data:
+ data["name"] = data.pop("new_name")
+ return super().update(id, data, **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/gitlab/v4/objects/merge_request_approvals.py
new/python_gitlab-8.2.0/gitlab/v4/objects/merge_request_approvals.py
--- old/python_gitlab-8.1.0/gitlab/v4/objects/merge_request_approvals.py
2026-02-28 02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/gitlab/v4/objects/merge_request_approvals.py
2026-03-28 02:49:29.000000000 +0100
@@ -90,7 +90,13 @@
_from_parent_attrs = {"project_id": "id"}
_create_attrs = RequiredOptional(
required=("name", "approvals_required"),
- optional=("user_ids", "group_ids", "protected_branch_ids",
"usernames"),
+ optional=(
+ "user_ids",
+ "group_ids",
+ "protected_branch_ids",
+ "usernames",
+ "applies_to_all_protected_branches",
+ ),
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/gitlab/v4/objects/projects.py
new/python_gitlab-8.2.0/gitlab/v4/objects/projects.py
--- old/python_gitlab-8.1.0/gitlab/v4/objects/projects.py 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/gitlab/v4/objects/projects.py 2026-03-28
02:49:29.000000000 +0100
@@ -49,6 +49,8 @@
)
from .events import ProjectEventManager # noqa: F401
from .export_import import ProjectExportManager, ProjectImportManager # noqa:
F401
+from .feature_flag_user_lists import ProjectFeatureFlagUserListManager #
noqa: F401
+from .feature_flags import ProjectFeatureFlagManager # noqa: F401
from .files import ProjectFileManager # noqa: F401
from .hooks import ProjectHookManager # noqa: F401
from .integrations import ProjectIntegrationManager, ProjectServiceManager #
noqa: F401
@@ -201,6 +203,8 @@
environments: ProjectEnvironmentManager
events: ProjectEventManager
exports: ProjectExportManager
+ feature_flags: ProjectFeatureFlagManager
+ feature_flags_user_lists: ProjectFeatureFlagUserListManager
files: ProjectFileManager
forks: ProjectForkManager
generic_packages: GenericPackageManager
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/python_gitlab.egg-info/PKG-INFO
new/python_gitlab-8.2.0/python_gitlab.egg-info/PKG-INFO
--- old/python_gitlab-8.1.0/python_gitlab.egg-info/PKG-INFO 2026-02-28
02:26:14.000000000 +0100
+++ new/python_gitlab-8.2.0/python_gitlab.egg-info/PKG-INFO 2026-03-28
02:49:45.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: python-gitlab
-Version: 8.1.0
+Version: 8.2.0
Summary: The python wrapper for the GitLab REST and GraphQL APIs.
Author-email: Gauvain Pocentek <[email protected]>
Maintainer-email: John Villalovos <[email protected]>, Max Wittig
<[email protected]>, Nejc Habjan <[email protected]>, Roger Meier
<[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/python_gitlab.egg-info/SOURCES.txt
new/python_gitlab-8.2.0/python_gitlab.egg-info/SOURCES.txt
--- old/python_gitlab-8.1.0/python_gitlab.egg-info/SOURCES.txt 2026-02-28
02:26:14.000000000 +0100
+++ new/python_gitlab-8.2.0/python_gitlab.egg-info/SOURCES.txt 2026-03-28
02:49:46.000000000 +0100
@@ -53,8 +53,8 @@
docs/gl_objects/environments.rst
docs/gl_objects/epics.rst
docs/gl_objects/events.rst
-docs/gl_objects/features.rst
docs/gl_objects/geo_nodes.rst
+docs/gl_objects/gitlab_features.rst
docs/gl_objects/group_access_tokens.rst
docs/gl_objects/groups.rst
docs/gl_objects/invitations.rst
@@ -77,6 +77,8 @@
docs/gl_objects/personal_access_tokens.rst
docs/gl_objects/pipelines_and_jobs.rst
docs/gl_objects/project_access_tokens.rst
+docs/gl_objects/project_feature_flag_user_lists.rst
+docs/gl_objects/project_feature_flags.rst
docs/gl_objects/projects.rst
docs/gl_objects/protected_branches.rst
docs/gl_objects/protected_container_repositories.rst
@@ -149,6 +151,8 @@
gitlab/v4/objects/epics.py
gitlab/v4/objects/events.py
gitlab/v4/objects/export_import.py
+gitlab/v4/objects/feature_flag_user_lists.py
+gitlab/v4/objects/feature_flags.py
gitlab/v4/objects/features.py
gitlab/v4/objects/files.py
gitlab/v4/objects/geo_nodes.py
@@ -233,6 +237,8 @@
tests/functional/api/test_member_roles.py
tests/functional/api/test_merge_requests.py
tests/functional/api/test_packages.py
+tests/functional/api/test_project_feature_flag_user_lists.py
+tests/functional/api/test_project_feature_flags.py
tests/functional/api/test_project_job_token_scope.py
tests/functional/api/test_projects.py
tests/functional/api/test_push_rules.py
@@ -252,6 +258,8 @@
tests/functional/cli/test_cli_artifacts.py
tests/functional/cli/test_cli_files.py
tests/functional/cli/test_cli_packages.py
+tests/functional/cli/test_cli_project_feature_flag_user_lists.py
+tests/functional/cli/test_cli_project_feature_flags.py
tests/functional/cli/test_cli_projects.py
tests/functional/cli/test_cli_repository.py
tests/functional/cli/test_cli_resource_access_tokens.py
@@ -332,6 +340,8 @@
tests/unit/objects/test_pipeline_schedules.py
tests/unit/objects/test_pipelines.py
tests/unit/objects/test_project_access_tokens.py
+tests/unit/objects/test_project_feature_flag_user_lists.py
+tests/unit/objects/test_project_feature_flags.py
tests/unit/objects/test_project_import_export.py
tests/unit/objects/test_project_merge_request_approvals.py
tests/unit/objects/test_project_statistics.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/requirements-lint.txt
new/python_gitlab-8.2.0/requirements-lint.txt
--- old/python_gitlab-8.1.0/requirements-lint.txt 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/requirements-lint.txt 2026-03-28
02:49:29.000000000 +0100
@@ -1,9 +1,9 @@
-r requirements.txt
argcomplete==2.0.0
-black==26.1.0
-commitizen==4.13.8
+black==26.3.1
+commitizen==4.13.9
flake8==7.3.0
-isort==8.0.0
+isort==8.0.1
mypy==1.19.1
pylint==4.0.5
pytest==9.0.2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/requirements-test.txt
new/python_gitlab-8.2.0/requirements-test.txt
--- old/python_gitlab-8.1.0/requirements-test.txt 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/requirements-test.txt 2026-03-28
02:49:29.000000000 +0100
@@ -1,10 +1,10 @@
-r requirements.txt
anyio==4.12.1
build==1.4.0
-coverage==7.13.4
+coverage==7.13.5
pytest-console-scripts==1.4.1
-pytest-cov==7.0.0
-pytest-github-actions-annotate-failures==0.3.0
+pytest-cov==7.1.0
+pytest-github-actions-annotate-failures==0.4.0
pytest==9.0.2
PyYaml==6.0.3
responses==0.26.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/requirements.txt
new/python_gitlab-8.2.0/requirements.txt
--- old/python_gitlab-8.1.0/requirements.txt 2026-02-28 02:25:57.000000000
+0100
+++ new/python_gitlab-8.2.0/requirements.txt 2026-03-28 02:49:29.000000000
+0100
@@ -1,4 +1,4 @@
gql==4.0.0
httpx==0.28.1
-requests==2.32.5
+requests==2.33.0
requests-toolbelt==1.0.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/tests/functional/api/test_project_feature_flag_user_lists.py
new/python_gitlab-8.2.0/tests/functional/api/test_project_feature_flag_user_lists.py
---
old/python_gitlab-8.1.0/tests/functional/api/test_project_feature_flag_user_lists.py
1970-01-01 01:00:00.000000000 +0100
+++
new/python_gitlab-8.2.0/tests/functional/api/test_project_feature_flag_user_lists.py
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,56 @@
+import pytest
+
+from gitlab import exceptions
+
+
[email protected]
+def user_list(project, user):
+ user_list = project.feature_flags_user_lists.create(
+ {"name": "test_user_list", "user_xids": str(user.id)}
+ )
+ yield user_list
+ try:
+ user_list.delete()
+ except exceptions.GitlabDeleteError:
+ pass
+
+
+def test_create_user_list(project, user):
+ user_list = project.feature_flags_user_lists.create(
+ {"name": "created_user_list", "user_xids": str(user.id)}
+ )
+ assert user_list.name == "created_user_list"
+ assert str(user.id) in user_list.user_xids
+ user_list.delete()
+
+
+def test_list_user_lists(project, user_list):
+ ff_user_lists = project.feature_flags_user_lists.list()
+ assert len(ff_user_lists) >= 1
+ assert user_list.iid in [ff_user.iid for ff_user in ff_user_lists]
+
+
+def test_get_user_list(project, user_list, user):
+ retrieved_list = project.feature_flags_user_lists.get(user_list.iid)
+ assert retrieved_list.name == user_list.name
+ assert str(user.id) in retrieved_list.user_xids
+
+
+def test_update_user_list(project, user_list):
+ user_list.name = "updated_user_list"
+ user_list.save()
+
+ updated_list = project.feature_flags_user_lists.get(user_list.iid)
+ assert updated_list.name == "updated_user_list"
+
+
+def test_delete_user_list(project, user_list):
+ user_list.delete()
+ with pytest.raises(exceptions.GitlabGetError):
+ project.feature_flags_user_lists.get(user_list.iid)
+
+
+def test_search_user_list(project, user_list):
+ ff_user_lists =
project.feature_flags_user_lists.list(search=user_list.name)
+ assert len(ff_user_lists) >= 1
+ assert user_list.iid in [ff_user.iid for ff_user in ff_user_lists]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/tests/functional/api/test_project_feature_flags.py
new/python_gitlab-8.2.0/tests/functional/api/test_project_feature_flags.py
--- old/python_gitlab-8.1.0/tests/functional/api/test_project_feature_flags.py
1970-01-01 01:00:00.000000000 +0100
+++ new/python_gitlab-8.2.0/tests/functional/api/test_project_feature_flags.py
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,127 @@
+import pytest
+
+from gitlab import exceptions
+
+
[email protected]
+def feature_flag(project):
+ flag_name = "test_flag_fixture"
+ flag = project.feature_flags.create(
+ {"name": flag_name, "version": "new_version_flag"}
+ )
+ yield flag
+ try:
+ flag.delete()
+ except exceptions.GitlabDeleteError:
+ pass
+
+
+def test_create_feature_flag(project):
+ flag_name = "test_flag_create"
+ flag = project.feature_flags.create(
+ {"name": flag_name, "version": "new_version_flag"}
+ )
+ assert flag.name == flag_name
+ assert flag.active is True
+ flag.delete()
+
+
+def test_create_feature_flag_with_strategies(project):
+ flag_name = "test_flag_strategies"
+ strategies = [{"name": "userWithId", "parameters": {"userIds": "user1"}}]
+ flag = project.feature_flags.create(
+ {"name": flag_name, "version": "new_version_flag", "strategies":
strategies}
+ )
+ assert len(flag.strategies) == 1
+ assert flag.strategies[0]["name"] == "userWithId"
+ assert flag.strategies[0]["parameters"]["userIds"] == "user1"
+ flag.delete()
+
+
+def test_list_feature_flags(project, feature_flag):
+ flags = project.feature_flags.list()
+ assert len(flags) >= 1
+ assert feature_flag.name in [f.name for f in flags]
+
+
+def test_update_feature_flag(project, feature_flag):
+ feature_flag.active = False
+ feature_flag.save()
+
+ updated_flag = project.feature_flags.get(feature_flag.name)
+ assert updated_flag.active is False
+
+
+def test_rename_feature_flag(project, feature_flag):
+ # Rename via save()
+ new_name = "renamed_flag"
+ feature_flag.name = new_name
+ feature_flag.save()
+
+ updated_flag = project.feature_flags.get(new_name)
+ assert updated_flag.name == new_name
+
+ # Rename via update()
+ newer_name = "renamed_flag_2"
+ project.feature_flags.update(new_name, {"name": newer_name})
+
+ updated_flag_2 = project.feature_flags.get(newer_name)
+ assert updated_flag_2.name == newer_name
+
+ # Update the fixture object so teardown can delete the correct flag
+ feature_flag.name = newer_name
+
+
+def test_delete_feature_flag(project, feature_flag):
+ feature_flag.delete()
+ with pytest.raises(exceptions.GitlabGetError):
+ project.feature_flags.get(feature_flag.name)
+
+
+def test_delete_feature_flag_strategy(project, feature_flag):
+ strategies = [
+ {"name": "default", "parameters": {}},
+ {"name": "userWithId", "parameters": {"userIds": "user1"}},
+ ]
+ feature_flag.strategies = strategies
+ feature_flag.save()
+
+ updated_feature_flag = project.feature_flags.get(feature_flag.name)
+ assert len(updated_feature_flag.strategies) == 2
+
+ # Remove strategy using _destroy
+ updated_strategies = updated_feature_flag.strategies
+ for strategy in updated_strategies:
+ if strategy["name"] == "userWithId":
+ strategy["_destroy"] = True
+ updated_feature_flag.save()
+
+ updated_feature_flag = project.feature_flags.get(feature_flag.name)
+ assert len(updated_feature_flag.strategies) == 1
+ assert updated_feature_flag.strategies[0]["name"] == "default"
+
+
+def test_delete_feature_flag_scope(project, feature_flag):
+ strategies = [
+ {
+ "name": "default",
+ "parameters": {},
+ "scopes": [{"environment_scope": "*"}, {"environment_scope":
"production"}],
+ }
+ ]
+ feature_flag.strategies = strategies
+ feature_flag.save()
+
+ updated_feature_flag = project.feature_flags.get(feature_flag.name)
+ assert len(updated_feature_flag.strategies[0]["scopes"]) == 2
+
+ # Remove scope using _destroy
+ updated_strategies = updated_feature_flag.strategies
+ for scope in updated_strategies[0]["scopes"]:
+ if scope["environment_scope"] == "production":
+ scope["_destroy"] = True
+ updated_feature_flag.save()
+
+ updated_feature_flag = project.feature_flags.get(feature_flag.name)
+ assert len(updated_feature_flag.strategies[0]["scopes"]) == 1
+ assert
updated_feature_flag.strategies[0]["scopes"][0]["environment_scope"] == "*"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/tests/functional/cli/test_cli_project_feature_flag_user_lists.py
new/python_gitlab-8.2.0/tests/functional/cli/test_cli_project_feature_flag_user_lists.py
---
old/python_gitlab-8.1.0/tests/functional/cli/test_cli_project_feature_flag_user_lists.py
1970-01-01 01:00:00.000000000 +0100
+++
new/python_gitlab-8.2.0/tests/functional/cli/test_cli_project_feature_flag_user_lists.py
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,120 @@
+import json
+
+import pytest
+
+
[email protected]
+def user_list_cli(gitlab_cli, project, user):
+ list_name = "cli_test_list_fixture"
+ cmd = [
+ "-o",
+ "json",
+ "project-feature-flag-user-list",
+ "create",
+ "--project-id",
+ str(project.id),
+ "--name",
+ list_name,
+ "--user-xids",
+ str(user.id),
+ ]
+ ret = gitlab_cli(cmd)
+ data = json.loads(ret.stdout)
+ iid = str(data["iid"])
+
+ yield iid
+
+ try:
+ cmd = [
+ "project-feature-flag-user-list",
+ "delete",
+ "--project-id",
+ str(project.id),
+ "--iid",
+ iid,
+ ]
+ gitlab_cli(cmd)
+ except Exception:
+ pass
+
+
+def test_project_feature_flag_user_list_cli_create_delete(gitlab_cli, project,
user):
+ list_name = "cli_test_list_create"
+
+ cmd = [
+ "-o",
+ "json",
+ "project-feature-flag-user-list",
+ "create",
+ "--project-id",
+ str(project.id),
+ "--name",
+ list_name,
+ "--user-xids",
+ str(user.id),
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ data = json.loads(ret.stdout)
+ assert data["name"] == list_name
+ assert str(user.id) in data["user_xids"]
+ iid = str(data["iid"])
+
+ cmd = [
+ "project-feature-flag-user-list",
+ "delete",
+ "--project-id",
+ str(project.id),
+ "--iid",
+ iid,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+
+
+def test_project_feature_flag_user_list_cli_list(gitlab_cli, project,
user_list_cli):
+ cmd = [
+ "-o",
+ "json",
+ "project-feature-flag-user-list",
+ "list",
+ "--project-id",
+ str(project.id),
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ data = json.loads(ret.stdout)
+ assert any(item["name"] == "cli_test_list_fixture" for item in data)
+
+
+def test_project_feature_flag_user_list_cli_get(gitlab_cli, project,
user_list_cli):
+ cmd = [
+ "-o",
+ "json",
+ "project-feature-flag-user-list",
+ "get",
+ "--project-id",
+ str(project.id),
+ "--iid",
+ user_list_cli,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ data = json.loads(ret.stdout)
+ assert data["name"] == "cli_test_list_fixture"
+
+
+def test_project_feature_flag_user_list_cli_update(gitlab_cli, project,
user_list_cli):
+ new_name = "cli_updated_list"
+ cmd = [
+ "project-feature-flag-user-list",
+ "update",
+ "--project-id",
+ str(project.id),
+ "--iid",
+ user_list_cli,
+ "--name",
+ new_name,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/tests/functional/cli/test_cli_project_feature_flags.py
new/python_gitlab-8.2.0/tests/functional/cli/test_cli_project_feature_flags.py
---
old/python_gitlab-8.1.0/tests/functional/cli/test_cli_project_feature_flags.py
1970-01-01 01:00:00.000000000 +0100
+++
new/python_gitlab-8.2.0/tests/functional/cli/test_cli_project_feature_flags.py
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,203 @@
+import json
+
+import pytest
+
+
[email protected]
+def feature_flag_cli(gitlab_cli, project):
+ flag_name = "test_flag_cli_fixture"
+ cmd = [
+ "project-feature-flag",
+ "create",
+ "--project-id",
+ str(project.id),
+ "--name",
+ flag_name,
+ ]
+ gitlab_cli(cmd)
+ yield flag_name
+ try:
+ cmd = [
+ "project-feature-flag",
+ "delete",
+ "--project-id",
+ str(project.id),
+ "--name",
+ flag_name,
+ ]
+ gitlab_cli(cmd)
+ except Exception:
+ pass
+
+
+def test_project_feature_flag_cli_create_delete(gitlab_cli, project):
+ flag_name = "test_flag_cli_create"
+ cmd = [
+ "project-feature-flag",
+ "create",
+ "--project-id",
+ str(project.id),
+ "--name",
+ flag_name,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ assert flag_name in ret.stdout
+
+ cmd = [
+ "project-feature-flag",
+ "delete",
+ "--project-id",
+ str(project.id),
+ "--name",
+ flag_name,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+
+
+def test_project_feature_flag_cli_create_with_strategies(gitlab_cli, project):
+ flag_name = "test_flag_cli_strategies"
+ strategies_json = (
+ '[{"name": "userWithId", "parameters": {"userIds": "user1,user2"}}]'
+ )
+
+ cmd = [
+ "project-feature-flag",
+ "create",
+ "--project-id",
+ str(project.id),
+ "--name",
+ flag_name,
+ "--strategies",
+ strategies_json,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+
+ cmd = [
+ "-o",
+ "json",
+ "project-feature-flag",
+ "get",
+ "--project-id",
+ str(project.id),
+ "--name",
+ flag_name,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ data = json.loads(ret.stdout)
+ assert len(data["strategies"]) == 1
+ assert data["strategies"][0]["name"] == "userWithId"
+
+
+def test_project_feature_flag_cli_list(gitlab_cli, project, feature_flag_cli):
+ cmd = ["project-feature-flag", "list", "--project-id", str(project.id)]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ assert feature_flag_cli in ret.stdout
+
+
+def test_project_feature_flag_cli_get(gitlab_cli, project, feature_flag_cli):
+ cmd = [
+ "project-feature-flag",
+ "get",
+ "--project-id",
+ str(project.id),
+ "--name",
+ feature_flag_cli,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ assert feature_flag_cli in ret.stdout
+
+
+def test_project_feature_flag_cli_update(gitlab_cli, project,
feature_flag_cli):
+ cmd = [
+ "project-feature-flag",
+ "update",
+ "--project-id",
+ str(project.id),
+ "--name",
+ feature_flag_cli,
+ "--active",
+ "false",
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+
+ cmd = [
+ "-o",
+ "json",
+ "project-feature-flag",
+ "get",
+ "--project-id",
+ str(project.id),
+ "--name",
+ feature_flag_cli,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ data = json.loads(ret.stdout)
+ assert data["active"] is False
+
+
+def test_project_feature_flag_cli_create_with_malformed_strategies(gitlab_cli,
project):
+ flag_name = "test_flag_cli_malformed_strategies"
+ strategies_json = '[{"name": "userWithId"' # Malformed JSON
+
+ cmd = [
+ "project-feature-flag",
+ "create",
+ "--project-id",
+ str(project.id),
+ "--name",
+ flag_name,
+ "--strategies",
+ strategies_json,
+ ]
+ ret = gitlab_cli(cmd)
+ assert not ret.success
+ assert "Could not parse JSON data" in ret.stderr
+
+
+def test_project_feature_flag_cli_rename(gitlab_cli, project,
feature_flag_cli):
+ new_name = "cli_renamed_flag"
+ cmd = [
+ "project-feature-flag",
+ "update",
+ "--project-id",
+ str(project.id),
+ "--name",
+ feature_flag_cli,
+ "--new-name",
+ new_name,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+
+ cmd = [
+ "-o",
+ "json",
+ "project-feature-flag",
+ "get",
+ "--project-id",
+ str(project.id),
+ "--name",
+ new_name,
+ ]
+ ret = gitlab_cli(cmd)
+ assert ret.success
+ data = json.loads(ret.stdout)
+ assert data["name"] == new_name
+ # Cleanup renamed flag
+ cmd = [
+ "project-feature-flag",
+ "delete",
+ "--project-id",
+ str(project.id),
+ "--name",
+ new_name,
+ ]
+ gitlab_cli(cmd)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/tests/functional/fixtures/.env
new/python_gitlab-8.2.0/tests/functional/fixtures/.env
--- old/python_gitlab-8.1.0/tests/functional/fixtures/.env 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/tests/functional/fixtures/.env 2026-03-28
02:49:29.000000000 +0100
@@ -1,4 +1,4 @@
GITLAB_IMAGE=gitlab/gitlab-ee
-GITLAB_TAG=18.9.0-ee.0
+GITLAB_TAG=18.9.2-ee.0
GITLAB_RUNNER_IMAGE=gitlab/gitlab-runner
GITLAB_RUNNER_TAG=96856197
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/tests/unit/objects/test_project_feature_flag_user_lists.py
new/python_gitlab-8.2.0/tests/unit/objects/test_project_feature_flag_user_lists.py
---
old/python_gitlab-8.1.0/tests/unit/objects/test_project_feature_flag_user_lists.py
1970-01-01 01:00:00.000000000 +0100
+++
new/python_gitlab-8.2.0/tests/unit/objects/test_project_feature_flag_user_lists.py
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,30 @@
+"""
+Unit tests for Project Feature Flag User Lists.
+"""
+
+import responses
+
+
+def test_create_user_list_with_list_conversion(project):
+ """
+ Verify that passing a list of integers for user_xids is converted
+ to a comma-separated string in the API payload.
+ """
+ with responses.RequestsMock() as rs:
+ rs.add(
+ responses.POST,
+ "http://localhost/api/v4/projects/1/feature_flags_user_lists",
+ json={"iid": 1, "name": "list", "user_xids": "1,2,3"},
+ status=201,
+ match=[
+ responses.matchers.json_params_matcher(
+ {"name": "list", "user_xids": "1,2,3"}
+ )
+ ],
+ )
+
+ project.feature_flags_user_lists.create(
+ {"name": "list", "user_xids": [1, 2, 3]}
+ )
+
+ assert len(rs.calls) == 1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_gitlab-8.1.0/tests/unit/objects/test_project_feature_flags.py
new/python_gitlab-8.2.0/tests/unit/objects/test_project_feature_flags.py
--- old/python_gitlab-8.1.0/tests/unit/objects/test_project_feature_flags.py
1970-01-01 01:00:00.000000000 +0100
+++ new/python_gitlab-8.2.0/tests/unit/objects/test_project_feature_flags.py
2026-03-28 02:49:29.000000000 +0100
@@ -0,0 +1,35 @@
+"""
+Unit tests for Project Feature Flags.
+"""
+
+import responses
+
+from gitlab.v4.objects import ProjectFeatureFlag
+
+
+def test_feature_flag_rename(project):
+ """
+ Verify that renaming a feature flag uses the old name in the URL
+ and the new name in the payload.
+ """
+ flag_content = {"name": "old_name", "version": "new_version_flag",
"active": True}
+ flag = ProjectFeatureFlag(project.feature_flags, flag_content)
+
+ # Rename locally
+ flag.name = "new_name"
+
+ with responses.RequestsMock() as rs:
+ rs.add(
+ responses.PUT,
+ "http://localhost/api/v4/projects/1/feature_flags/old_name",
+ json={"name": "new_name", "version": "new_version_flag", "active":
True},
+ status=200,
+ match=[responses.matchers.json_params_matcher({"name":
"new_name"})],
+ )
+
+ flag.save()
+
+ assert len(rs.calls) == 1
+ # URL should use the old name (ID)
+ assert rs.calls[0].request.url.endswith("/feature_flags/old_name")
+ assert flag.name == "new_name"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_gitlab-8.1.0/tests/unit/test_types.py
new/python_gitlab-8.2.0/tests/unit/test_types.py
--- old/python_gitlab-8.1.0/tests/unit/test_types.py 2026-02-28
02:25:57.000000000 +0100
+++ new/python_gitlab-8.2.0/tests/unit/test_types.py 2026-03-28
02:49:29.000000000 +0100
@@ -1,6 +1,6 @@
import pytest
-from gitlab import types
+from gitlab import exceptions, types
class TestRequiredOptional:
@@ -122,3 +122,40 @@
def test_lowercase_string_attribute_get_for_api():
o = types.LowercaseStringAttribute("FOO")
assert o.get_for_api(key="spam") == ("spam", "foo")
+
+
+# JsonAttribute tests
+def test_json_attribute() -> None:
+ attr = types.JsonAttribute()
+
+ attr.set_from_cli('{"key": "value"}')
+ assert attr.get() == {"key": "value"}
+
+ with pytest.raises(exceptions.GitlabParsingError):
+ attr.set_from_cli(" ")
+
+
+# CommaSeparatedStringAttribute tests
+def test_comma_separated_string_attribute() -> None:
+ # Test with list of integers
+ attr = types.CommaSeparatedStringAttribute([1, 2, 3])
+ assert attr.get_for_api(key="ids") == ("ids", "1,2,3")
+
+ # Test with list of strings
+ attr = types.CommaSeparatedStringAttribute(["a", "b"])
+ assert attr.get_for_api(key="names") == ("names", "a,b")
+
+ # Test with string value (should be preserved)
+ attr = types.CommaSeparatedStringAttribute("1,2,3")
+ assert attr.get_for_api(key="ids") == ("ids", "1,2,3")
+
+ # Test CLI setting
+ attr = types.CommaSeparatedStringAttribute()
+ attr.set_from_cli("1, 2, 3")
+ assert attr.get() == ["1", "2", "3"]
+
+ attr.set_from_cli("")
+ assert attr.get() == []
+
+ # Verify transform_in_body is True
+ assert types.CommaSeparatedStringAttribute.transform_in_body is True