Skip to content

Commit e2f7374

Browse files
committed
Modernize to Python 3.11+ with UV and Ruff
This is a comprehensive modernization of python-gerritclient to modern Python standards and tooling as of 2025. ## Major Changes ### Python Version Support - **Drop support for Python 2.7, 3.5, 3.6** - **Add support for Python 3.11, 3.12, 3.13** - Bump version to 1.0.0 to signify breaking change ### Dependency Management & Tooling - Add `pyproject.toml` with UV + Ruff configuration - Replace pip/virtualenv workflow with UV (10-100x faster) - Replace flake8/hacking with Ruff (100x faster linting) - Replace black with Ruff format (30x faster, >99.9% compatible) - Add comprehensive tool configuration in pyproject.toml ### Code Modernization - Remove `six` library dependency (12 occurrences across 7 files) - Replace `@six.add_metaclass(abc.ABCMeta)` with `class Foo(abc.ABC)` - Replace deprecated `@abc.abstractproperty` with `@property @abstractmethod` (9 occurrences) - Remove explicit `object` inheritance (unnecessary in Python 3) - Fix `dict.values()` bug in base.py:86 - wrap with `list()` for Python 3 compatibility - Replace `six.text_type()` with built-in `str()` ### Testing Infrastructure - Replace standalone `mock` with `unittest.mock` (7 test files updated) - Update tox.ini for Python 3.11-3.13 testing - Add pytest and pytest-cov as test dependencies - Add mypy for type checking support ### CI/CD - Add GitHub Actions workflow (.github/workflows/test.yml) - Test matrix for Python 3.11, 3.12, 3.13 - Automated linting, formatting checks, and test coverage - Replace Travis CI configuration ### Documentation - Update README.md with modern installation instructions - Add UV installation and usage guide - Document Python 3.11+ requirement - Provide both UV and pip installation methods ### Code Quality - All code formatted with Ruff (38 files reformatted) - All code checked and auto-fixed with Ruff linter - Removed deprecated Python 2/3 compatibility patterns - Modernized string formatting (ruff auto-fixes applied) ## Files Modified - 44 Python source files updated - 3 new files created (pyproject.toml, .github/workflows/test.yml, uv.lock) - 3 configuration files updated (setup.cfg, tox.ini, README.md) ## Testing - ✅ `uv sync` completes successfully - ✅ `uv run gerrit --help` works correctly - ✅ All dependencies install cleanly - ✅ Code is properly formatted and linted ## Breaking Changes - **Python 2.7 no longer supported** - **Python 3.5, 3.6 no longer supported** - **Minimum Python version is now 3.11** - six dependency removed (breaking for any code importing six) This modernization sets up python-gerritclient for long-term maintenance and positions it as a modern, well-maintained Python 3.11+ project.
1 parent 8da463b commit e2f7374

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3556
-2937
lines changed

.github/workflows/test.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main, claude/**]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.11", "3.12", "3.13"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v5
21+
with:
22+
version: "latest"
23+
24+
- name: Set up Python ${{ matrix.python-version }}
25+
run: uv python install ${{ matrix.python-version }}
26+
27+
- name: Install dependencies
28+
run: uv sync --all-extras
29+
30+
- name: Lint with ruff
31+
run: uv run ruff check .
32+
33+
- name: Format check with ruff
34+
run: uv run ruff format --check .
35+
36+
- name: Run tests
37+
run: uv run pytest --cov=gerritclient --cov-report=term-missing --cov-report=xml
38+
39+
- name: Upload coverage to Codecov
40+
uses: codecov/codecov-action@v4
41+
if: matrix.python-version == '3.11'
42+
with:
43+
file: ./coverage.xml
44+
fail_ci_if_error: false

README.md

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,84 @@
55
# python-gerritclient
66
CLI tool and Python API wrapper for Gerrit Code Review
77

8+
## Requirements
9+
10+
**Python 3.11+** is required. This project uses modern Python features and tooling.
11+
812
## Quick Start
913

10-
### Command Line Tool
11-
1. Clone `python-gerritclient` repository: `git clone https://github.com/tivaliy/python-gerritclient.git`.
12-
2. Configure `settings.yaml` file (in `gerritclient/settings.yaml`) to meet your requirements.
14+
### Command Line Tool (Recommended: Using UV)
15+
16+
[UV](https://docs.astral.sh/uv/) is a fast, modern Python package manager. Recommended for the best experience.
17+
18+
1. Install UV (if not already installed):
19+
```bash
20+
curl -LsSf https://astral.sh/uv/install.sh | sh
21+
```
1322

23+
2. Clone the repository:
24+
```bash
25+
git clone https://github.com/tivaliy/python-gerritclient.git
26+
cd python-gerritclient
27+
```
28+
29+
3. Configure `settings.yaml` file (in `gerritclient/settings.yaml`):
1430
```yaml
15-
url: http://review.example.com
16-
auth_type: basic
17-
username: admin
18-
password: "1234567890aaWmmflSl+ZlOPs23Dffn"
31+
url: http://review.example.com
32+
auth_type: basic
33+
username: admin
34+
password: "1234567890aaWmmflSl+ZlOPs23Dffn"
1935
```
2036
21-
* `url` can be specified according to the following format `<scheme>://<host>:<port>`, e.g. `https://review.openstack.org`
22-
* `auth_type` specifies HTTP authentication scheme (`basic` or `digest`), can be omitted, then all requests will be anonymous with respective restrictions
23-
* `username` and `password` - user credentials from Gerrit system (Settings &#8594; HTTP Password)
37+
* `url` - Gerrit server URL in format `<scheme>://<host>:<port>` (e.g., `https://review.openstack.org`)
38+
* `auth_type` - HTTP authentication scheme (`basic` or `digest`), omit for anonymous access
39+
* `username` and `password` - user credentials from Gerrit (Settings → HTTP Password)
40+
41+
4. Install dependencies and run:
42+
```bash
43+
uv sync
44+
uv run gerrit --help
45+
```
46+
47+
5. Run commands:
48+
```bash
49+
uv run gerrit plugin list
50+
uv run gerrit account list "john"
51+
```
2452

25-
3. Create isolated Python environment `virtualenv gerritclient_venv` and activate it `source gerritclient_venv/bin/activate`.
26-
4. Install `python-gerritclient` with all necessary dependencies: `pip install python-gerritclient/.`.
27-
5. (Optional) Add gerrit command bash completion `gerrit complete | sudo tee /etc/bash_completion.d/gc.bash_completion > /dev/null`
28-
6. Run `gerrit` command with required options, e.g. `gerrit plugin list`. To see all available commands run `gerrit --help`.
53+
### Command Line Tool (Alternative: Using pip)
2954

30-
### Library
31-
1. Clone `python-gerritclient` repository: `git clone https://github.com/tivaliy/python-gerritclient.git`.
32-
2. Create isolated Python environment `virtualenv gerritclient_venv` and activate it `source gerritclient_venv/bin/activate`.
33-
3. Install `python-gerritclient` with all necessary dependencies: `pip install python-gerritclient/.`.
55+
1. Clone the repository:
56+
```bash
57+
git clone https://github.com/tivaliy/python-gerritclient.git
58+
cd python-gerritclient
59+
```
60+
61+
2. Configure `settings.yaml` (same as above)
62+
63+
3. Install with pip:
64+
```bash
65+
python3 -m venv venv
66+
source venv/bin/activate # On Windows: venv\Scripts\activate
67+
pip install -e .
68+
```
69+
70+
4. Run commands:
71+
```bash
72+
gerrit --help
73+
gerrit plugin list
74+
```
75+
76+
### Library Usage
77+
78+
Install the package:
79+
```bash
80+
# With UV
81+
uv add python-gerritclient
82+
83+
# With pip
84+
pip install python-gerritclient
85+
```
3486

3587
```python
3688
from gerritclient import client

gerritclient/client.py

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,16 @@
1515

1616
import json
1717
import os
18-
import requests
19-
20-
import gerritclient
2118

19+
import requests
2220
from requests import auth
2321

24-
from gerritclient.common import utils
22+
import gerritclient
2523
from gerritclient import error
24+
from gerritclient.common import utils
2625

2726

28-
class APIClient(object):
27+
class APIClient:
2928
"""This class handles API requests."""
3029

3130
def __init__(self, url, auth_type=None, username=None, password=None):
@@ -49,11 +48,10 @@ def __init__(self, url, auth_type=None, username=None, password=None):
4948
self._auth = None
5049
if auth_type:
5150
if not all((self._username, self._password)):
52-
raise ValueError('Username and password must be specified.')
53-
auth_types = {'basic': auth.HTTPBasicAuth,
54-
'digest': auth.HTTPDigestAuth}
51+
raise ValueError("Username and password must be specified.")
52+
auth_types = {"basic": auth.HTTPBasicAuth, "digest": auth.HTTPDigestAuth}
5553
if auth_type not in auth_types:
56-
raise ValueError('Unsupported auth_type {}'.format(auth_type))
54+
raise ValueError(f"Unsupported auth_type {auth_type}")
5755
self._auth = auth_types[auth_type](self._username, self._password)
5856

5957
if self.is_authed:
@@ -71,8 +69,7 @@ def is_authed(self):
7169
def _make_common_headers():
7270
"""Returns a dict of HTTP headers common for all requests."""
7371

74-
return {'Content-Type': 'application/json',
75-
'Accept': 'application/json'}
72+
return {"Content-Type": "application/json", "Accept": "application/json"}
7673

7774
def _make_session(self):
7875
"""Initializes a HTTP session."""
@@ -135,8 +132,7 @@ def get_request(self, api, params=None):
135132
self._raise_for_status_with_info(resp)
136133
return self._decode_content(resp)
137134

138-
def post_request_raw(self, api, data=None, json_data=None,
139-
content_type=None):
135+
def post_request_raw(self, api, data=None, json_data=None, content_type=None):
140136
"""Make a POST request to specific API and return raw response.
141137
142138
:param api: API endpoint (path)
@@ -150,12 +146,11 @@ def post_request_raw(self, api, data=None, json_data=None,
150146
# Some POST requests require 'Content-Type' value other
151147
# than default 'application/json'
152148
if content_type is not None:
153-
self.session.headers.update({'Content-Type': content_type})
149+
self.session.headers.update({"Content-Type": content_type})
154150

155151
return self.session.post(url, data=data, json=json_data)
156152

157-
def post_request(self, api, data=None, json_data=None,
158-
content_type=None):
153+
def post_request(self, api, data=None, json_data=None, content_type=None):
159154
"""Make POST request to specific API with some data."""
160155

161156
resp = self.post_request_raw(api, data, json_data, content_type)
@@ -175,7 +170,7 @@ def _decode_content(response):
175170
return {}
176171

177172
# Some responses can be of 'text/plain' Content-Type
178-
if 'text/plain' in response.headers.get('Content-Type'):
173+
if "text/plain" in response.headers.get("Content-Type"):
179174
return response.text
180175

181176
# Remove ")]}'" prefix from response, that is used to prevent XSSI
@@ -194,9 +189,10 @@ def get_settings(file_path=None):
194189

195190
config = None
196191

197-
user_config = os.path.join(os.path.expanduser('~'), '.config',
198-
'gerritclient', 'settings.yaml')
199-
local_config = os.path.join(os.path.dirname(__file__), 'settings.yaml')
192+
user_config = os.path.join(
193+
os.path.expanduser("~"), ".config", "gerritclient", "settings.yaml"
194+
)
195+
local_config = os.path.join(os.path.dirname(__file__), "settings.yaml")
200196

201197
if file_path is not None:
202198
config = file_path
@@ -211,22 +207,19 @@ def get_settings(file_path=None):
211207

212208
try:
213209
config_data = utils.read_from_file(config)
214-
except (OSError, IOError):
215-
msg = "Could not read settings from {0}".format(file_path)
210+
except OSError:
211+
msg = f"Could not read settings from {file_path}"
216212
raise error.InvalidFileException(msg)
217213
return config_data
218214

219215

220216
def connect(url, auth_type=None, username=None, password=None):
221217
"""Creates API connection."""
222218

223-
return APIClient(url,
224-
auth_type=auth_type,
225-
username=username,
226-
password=password)
219+
return APIClient(url, auth_type=auth_type, username=username, password=password)
227220

228221

229-
def get_client(resource, version='v1', connection=None):
222+
def get_client(resource, version="v1", connection=None):
230223
"""Gets an API client for a resource
231224
232225
python-gerritclient provides access to Gerrit Code Review's API
@@ -245,13 +238,13 @@ def get_client(resource, version='v1', connection=None):
245238
"""
246239

247240
version_map = {
248-
'v1': {
249-
'account': gerritclient.v1.account,
250-
'change': gerritclient.v1.change,
251-
'group': gerritclient.v1.group,
252-
'plugin': gerritclient.v1.plugin,
253-
'project': gerritclient.v1.project,
254-
'server': gerritclient.v1.server
241+
"v1": {
242+
"account": gerritclient.v1.account,
243+
"change": gerritclient.v1.change,
244+
"group": gerritclient.v1.group,
245+
"plugin": gerritclient.v1.plugin,
246+
"project": gerritclient.v1.project,
247+
"server": gerritclient.v1.server,
255248
}
256249
}
257250

0 commit comments

Comments
 (0)