Skip to content

Commit 170a4d9

Browse files
authored
Merge pull request #1743 from python-gitlab/feat/cli-without-config-file
feat(cli): do not require config file to run CLI
2 parents 74d4e4b + 92a893b commit 170a4d9

File tree

6 files changed

+234
-116
lines changed

6 files changed

+234
-116
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ repos:
2626
- id: pylint
2727
additional_dependencies:
2828
- argcomplete==1.12.3
29+
- pytest==6.2.5
2930
- requests==2.26.0
3031
- requests-toolbelt==0.9.1
3132
files: 'gitlab/'

docs/cli-usage.rst

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
``python-gitlab`` provides a :command:`gitlab` command-line tool to interact
66
with GitLab servers. It uses a configuration file to define how to connect to
7-
the servers.
7+
the servers. Without a configuration file, ``gitlab`` will default to
8+
https://gitlab.com and unauthenticated requests.
89

910
.. _cli_configuration:
1011

@@ -16,8 +17,8 @@ Files
1617

1718
``gitlab`` looks up 3 configuration files by default:
1819

19-
``PYTHON_GITLAB_CFG`` environment variable
20-
An environment variable that contains the path to a configuration file
20+
The ``PYTHON_GITLAB_CFG`` environment variable
21+
An environment variable that contains the path to a configuration file.
2122

2223
``/etc/python-gitlab.cfg``
2324
System-wide configuration file
@@ -27,6 +28,13 @@ Files
2728

2829
You can use a different configuration file with the ``--config-file`` option.
2930

31+
.. warning::
32+
If the ``PYTHON_GITLAB_CFG`` environment variable is defined and the target
33+
file exists, it will be the only configuration file parsed by ``gitlab``.
34+
35+
If the environment variable is defined and the target file cannot be accessed,
36+
``gitlab`` will fail explicitly.
37+
3038
Content
3139
-------
3240

gitlab/config.py

Lines changed: 94 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,67 @@
2020
import shlex
2121
import subprocess
2222
from os.path import expanduser, expandvars
23+
from pathlib import Path
2324
from typing import List, Optional, Union
2425

25-
from gitlab.const import USER_AGENT
26+
from gitlab.const import DEFAULT_URL, USER_AGENT
2627

27-
28-
def _env_config() -> List[str]:
29-
if "PYTHON_GITLAB_CFG" in os.environ:
30-
return [os.environ["PYTHON_GITLAB_CFG"]]
31-
return []
32-
33-
34-
_DEFAULT_FILES: List[str] = _env_config() + [
28+
_DEFAULT_FILES: List[str] = [
3529
"/etc/python-gitlab.cfg",
36-
os.path.expanduser("~/.python-gitlab.cfg"),
30+
str(Path.home() / ".python-gitlab.cfg"),
3731
]
3832

3933
HELPER_PREFIX = "helper:"
4034

4135
HELPER_ATTRIBUTES = ["job_token", "http_password", "private_token", "oauth_token"]
4236

4337

38+
def _resolve_file(filepath: Union[Path, str]) -> str:
39+
resolved = Path(filepath).resolve(strict=True)
40+
return str(resolved)
41+
42+
43+
def _get_config_files(
44+
config_files: Optional[List[str]] = None,
45+
) -> Union[str, List[str]]:
46+
"""
47+
Return resolved path(s) to config files if they exist, with precedence:
48+
1. Files passed in config_files
49+
2. File defined in PYTHON_GITLAB_CFG
50+
3. User- and system-wide config files
51+
"""
52+
resolved_files = []
53+
54+
if config_files:
55+
for config_file in config_files:
56+
try:
57+
resolved = _resolve_file(config_file)
58+
except OSError as e:
59+
raise GitlabConfigMissingError(f"Cannot read config from file: {e}")
60+
resolved_files.append(resolved)
61+
62+
return resolved_files
63+
64+
try:
65+
env_config = os.environ["PYTHON_GITLAB_CFG"]
66+
return _resolve_file(env_config)
67+
except KeyError:
68+
pass
69+
except OSError as e:
70+
raise GitlabConfigMissingError(
71+
f"Cannot read config from PYTHON_GITLAB_CFG: {e}"
72+
)
73+
74+
for config_file in _DEFAULT_FILES:
75+
try:
76+
resolved = _resolve_file(config_file)
77+
except OSError:
78+
continue
79+
resolved_files.append(resolved)
80+
81+
return resolved_files
82+
83+
4484
class ConfigError(Exception):
4585
pass
4686

@@ -66,155 +106,149 @@ def __init__(
66106
self, gitlab_id: Optional[str] = None, config_files: Optional[List[str]] = None
67107
) -> None:
68108
self.gitlab_id = gitlab_id
69-
_files = config_files or _DEFAULT_FILES
70-
file_exist = False
71-
for file in _files:
72-
if os.path.exists(file):
73-
file_exist = True
74-
if not file_exist:
75-
raise GitlabConfigMissingError(
76-
"Config file not found. \nPlease create one in "
77-
"one of the following locations: {} \nor "
78-
"specify a config file using the '-c' parameter.".format(
79-
", ".join(_DEFAULT_FILES)
80-
)
81-
)
109+
self.http_username: Optional[str] = None
110+
self.http_password: Optional[str] = None
111+
self.job_token: Optional[str] = None
112+
self.oauth_token: Optional[str] = None
113+
self.private_token: Optional[str] = None
114+
115+
self.api_version: str = "4"
116+
self.order_by: Optional[str] = None
117+
self.pagination: Optional[str] = None
118+
self.per_page: Optional[int] = None
119+
self.retry_transient_errors: bool = False
120+
self.ssl_verify: Union[bool, str] = True
121+
self.timeout: int = 60
122+
self.url: str = DEFAULT_URL
123+
self.user_agent: str = USER_AGENT
82124

83-
self._config = configparser.ConfigParser()
84-
self._config.read(_files)
125+
self._files = _get_config_files(config_files)
126+
if self._files:
127+
self._parse_config()
128+
129+
def _parse_config(self) -> None:
130+
_config = configparser.ConfigParser()
131+
_config.read(self._files)
85132

86133
if self.gitlab_id is None:
87134
try:
88-
self.gitlab_id = self._config.get("global", "default")
135+
self.gitlab_id = _config.get("global", "default")
89136
except Exception as e:
90137
raise GitlabIDError(
91138
"Impossible to get the gitlab id (not specified in config file)"
92139
) from e
93140

94141
try:
95-
self.url = self._config.get(self.gitlab_id, "url")
142+
self.url = _config.get(self.gitlab_id, "url")
96143
except Exception as e:
97144
raise GitlabDataError(
98145
"Impossible to get gitlab details from "
99146
f"configuration ({self.gitlab_id})"
100147
) from e
101148

102-
self.ssl_verify: Union[bool, str] = True
103149
try:
104-
self.ssl_verify = self._config.getboolean("global", "ssl_verify")
150+
self.ssl_verify = _config.getboolean("global", "ssl_verify")
105151
except ValueError:
106152
# Value Error means the option exists but isn't a boolean.
107153
# Get as a string instead as it should then be a local path to a
108154
# CA bundle.
109155
try:
110-
self.ssl_verify = self._config.get("global", "ssl_verify")
156+
self.ssl_verify = _config.get("global", "ssl_verify")
111157
except Exception:
112158
pass
113159
except Exception:
114160
pass
115161
try:
116-
self.ssl_verify = self._config.getboolean(self.gitlab_id, "ssl_verify")
162+
self.ssl_verify = _config.getboolean(self.gitlab_id, "ssl_verify")
117163
except ValueError:
118164
# Value Error means the option exists but isn't a boolean.
119165
# Get as a string instead as it should then be a local path to a
120166
# CA bundle.
121167
try:
122-
self.ssl_verify = self._config.get(self.gitlab_id, "ssl_verify")
168+
self.ssl_verify = _config.get(self.gitlab_id, "ssl_verify")
123169
except Exception:
124170
pass
125171
except Exception:
126172
pass
127173

128-
self.timeout = 60
129174
try:
130-
self.timeout = self._config.getint("global", "timeout")
175+
self.timeout = _config.getint("global", "timeout")
131176
except Exception:
132177
pass
133178
try:
134-
self.timeout = self._config.getint(self.gitlab_id, "timeout")
179+
self.timeout = _config.getint(self.gitlab_id, "timeout")
135180
except Exception:
136181
pass
137182

138-
self.private_token = None
139183
try:
140-
self.private_token = self._config.get(self.gitlab_id, "private_token")
184+
self.private_token = _config.get(self.gitlab_id, "private_token")
141185
except Exception:
142186
pass
143187

144-
self.oauth_token = None
145188
try:
146-
self.oauth_token = self._config.get(self.gitlab_id, "oauth_token")
189+
self.oauth_token = _config.get(self.gitlab_id, "oauth_token")
147190
except Exception:
148191
pass
149192

150-
self.job_token = None
151193
try:
152-
self.job_token = self._config.get(self.gitlab_id, "job_token")
194+
self.job_token = _config.get(self.gitlab_id, "job_token")
153195
except Exception:
154196
pass
155197

156-
self.http_username = None
157-
self.http_password = None
158198
try:
159-
self.http_username = self._config.get(self.gitlab_id, "http_username")
160-
self.http_password = self._config.get(self.gitlab_id, "http_password")
199+
self.http_username = _config.get(self.gitlab_id, "http_username")
200+
self.http_password = _config.get(self.gitlab_id, "http_password")
161201
except Exception:
162202
pass
163203

164204
self._get_values_from_helper()
165205

166-
self.api_version = "4"
167206
try:
168-
self.api_version = self._config.get("global", "api_version")
207+
self.api_version = _config.get("global", "api_version")
169208
except Exception:
170209
pass
171210
try:
172-
self.api_version = self._config.get(self.gitlab_id, "api_version")
211+
self.api_version = _config.get(self.gitlab_id, "api_version")
173212
except Exception:
174213
pass
175214
if self.api_version not in ("4",):
176215
raise GitlabDataError(f"Unsupported API version: {self.api_version}")
177216

178-
self.per_page = None
179217
for section in ["global", self.gitlab_id]:
180218
try:
181-
self.per_page = self._config.getint(section, "per_page")
219+
self.per_page = _config.getint(section, "per_page")
182220
except Exception:
183221
pass
184222
if self.per_page is not None and not 0 <= self.per_page <= 100:
185223
raise GitlabDataError(f"Unsupported per_page number: {self.per_page}")
186224

187-
self.pagination = None
188225
try:
189-
self.pagination = self._config.get(self.gitlab_id, "pagination")
226+
self.pagination = _config.get(self.gitlab_id, "pagination")
190227
except Exception:
191228
pass
192229

193-
self.order_by = None
194230
try:
195-
self.order_by = self._config.get(self.gitlab_id, "order_by")
231+
self.order_by = _config.get(self.gitlab_id, "order_by")
196232
except Exception:
197233
pass
198234

199-
self.user_agent = USER_AGENT
200235
try:
201-
self.user_agent = self._config.get("global", "user_agent")
236+
self.user_agent = _config.get("global", "user_agent")
202237
except Exception:
203238
pass
204239
try:
205-
self.user_agent = self._config.get(self.gitlab_id, "user_agent")
240+
self.user_agent = _config.get(self.gitlab_id, "user_agent")
206241
except Exception:
207242
pass
208243

209-
self.retry_transient_errors = False
210244
try:
211-
self.retry_transient_errors = self._config.getboolean(
245+
self.retry_transient_errors = _config.getboolean(
212246
"global", "retry_transient_errors"
213247
)
214248
except Exception:
215249
pass
216250
try:
217-
self.retry_transient_errors = self._config.getboolean(
251+
self.retry_transient_errors = _config.getboolean(
218252
self.gitlab_id, "retry_transient_errors"
219253
)
220254
except Exception:

requirements-test.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
coverage
22
httmock
3-
pytest
3+
pytest==6.2.5
44
pytest-console-scripts==1.2.1
55
pytest-cov
66
responses

tests/functional/cli/test_cli.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
import json
22

3+
import pytest
4+
import responses
5+
36
from gitlab import __version__
47

58

9+
@pytest.fixture
10+
def resp_get_project():
11+
with responses.RequestsMock() as rsps:
12+
rsps.add(
13+
method=responses.GET,
14+
url="https://gitlab.com/api/v4/projects/1",
15+
json={"name": "name", "path": "test-path", "id": 1},
16+
content_type="application/json",
17+
status=200,
18+
)
19+
yield rsps
20+
21+
622
def test_main_entrypoint(script_runner, gitlab_config):
723
ret = script_runner.run("python", "-m", "gitlab", "--config-file", gitlab_config)
824
assert ret.returncode == 2
@@ -13,6 +29,29 @@ def test_version(script_runner):
1329
assert ret.stdout.strip() == __version__
1430

1531

32+
@pytest.mark.script_launch_mode("inprocess")
33+
def test_defaults_to_gitlab_com(script_runner, resp_get_project):
34+
# Runs in-process to intercept requests to gitlab.com
35+
ret = script_runner.run("gitlab", "project", "get", "--id", "1")
36+
assert ret.success
37+
assert "id: 1" in ret.stdout
38+
39+
40+
def test_env_config_missing_file_raises(script_runner, monkeypatch):
41+
monkeypatch.setenv("PYTHON_GITLAB_CFG", "non-existent")
42+
ret = script_runner.run("gitlab", "project", "list")
43+
assert not ret.success
44+
assert ret.stderr.startswith("Cannot read config from PYTHON_GITLAB_CFG")
45+
46+
47+
def test_arg_config_missing_file_raises(script_runner):
48+
ret = script_runner.run(
49+
"gitlab", "--config-file", "non-existent", "project", "list"
50+
)
51+
assert not ret.success
52+
assert ret.stderr.startswith("Cannot read config from file")
53+
54+
1655
def test_invalid_config(script_runner):
1756
ret = script_runner.run("gitlab", "--gitlab", "invalid")
1857
assert not ret.success

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy