Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Commit 3060ddc

Browse files
committed
Added --config flag
1 parent 9f462f6 commit 3060ddc

File tree

3 files changed

+115
-38
lines changed

3 files changed

+115
-38
lines changed

src/pydocstyle/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ def run_pydocstyle():
4343
conf.get_files_to_check():
4444
errors.extend(check((filename,), select=checked_codes,
4545
ignore_decorators=ignore_decorators))
46-
except IllegalConfiguration:
46+
except IllegalConfiguration as error:
4747
# An illegal configuration file was found during file generation.
48+
log.error(error.args[0])
4849
return ReturnCode.invalid_options
4950

5051
count = 0

src/pydocstyle/config.py

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ class ConfigurationParser(object):
3434
3535
Run Configurations:
3636
------------------
37-
Responsible for deciding things that are related to the user interface,
38-
e.g. verbosity, debug options, etc.
39-
All run configurations default to `False` and are decided only by CLI.
37+
Responsible for deciding things that are related to the user interface and
38+
configuration discovery, e.g. verbosity, debug options, etc.
39+
All run configurations default to `False` or `None` and are decided only
40+
by CLI.
4041
4142
Check Configurations:
4243
--------------------
@@ -175,6 +176,48 @@ def _get_ignore_decorators(config):
175176

176177
# --------------------------- Private Methods -----------------------------
177178

179+
def _get_config_by_discovery(self, node):
180+
"""Get a configuration for checking `node` by config discovery.
181+
182+
Config discovery happens when no explicit config file is specified. The
183+
file system is searched for config files starting from the directory
184+
containing the file being checked, and up until the root directory of
185+
the project.
186+
187+
See `_get_config` for further details.
188+
189+
"""
190+
path = self._get_node_dir(node)
191+
192+
if path in self._cache:
193+
return self._cache[path]
194+
195+
config_file = self._get_config_file_in_folder(path)
196+
197+
if config_file is None:
198+
parent_dir, tail = os.path.split(path)
199+
if tail:
200+
# No configuration file, simply take the parent's.
201+
config = self._get_config(parent_dir)
202+
else:
203+
# There's no configuration file and no parent directory.
204+
# Use the default configuration or the one given in the CLI.
205+
config = self._create_check_config(self._options)
206+
else:
207+
# There's a config file! Read it and merge if necessary.
208+
options, inherit = self._read_configuration_file(config_file)
209+
210+
parent_dir, tail = os.path.split(path)
211+
if tail and inherit:
212+
# There is a parent dir and we should try to merge.
213+
parent_config = self._get_config(parent_dir)
214+
config = self._merge_configuration(parent_config, options)
215+
else:
216+
# No need to merge or parent dir does not exist.
217+
config = self._create_check_config(options)
218+
219+
return config
220+
178221
def _get_config(self, node):
179222
"""Get and cache the run configuration for `node`.
180223
@@ -204,35 +247,19 @@ def _get_config(self, node):
204247
* Set the `--add-select` and `--add-ignore` CLI configurations.
205248
206249
"""
207-
path = os.path.abspath(node)
208-
path = path if os.path.isdir(path) else os.path.dirname(path)
209-
210-
if path in self._cache:
211-
return self._cache[path]
212-
213-
config_file = self._get_config_file_in_folder(path)
214-
215-
if config_file is None:
216-
parent_dir, tail = os.path.split(path)
217-
if tail:
218-
# No configuration file, simply take the parent's.
219-
config = self._get_config(parent_dir)
220-
else:
221-
# There's no configuration file and no parent directory.
222-
# Use the default configuration or the one given in the CLI.
223-
config = self._create_check_config(self._options)
250+
if self._run_conf.config is None:
251+
log.debug('No config file specified, discovering.')
252+
config = self._get_config_by_discovery(node)
224253
else:
225-
# There's a config file! Read it and merge if necessary.
226-
options, inherit = self._read_configuration_file(config_file)
227-
228-
parent_dir, tail = os.path.split(path)
229-
if tail and inherit:
230-
# There is a parent dir and we should try to merge.
231-
parent_config = self._get_config(parent_dir)
232-
config = self._merge_configuration(parent_config, options)
233-
else:
234-
# No need to merge or parent dir does not exist.
235-
config = self._create_check_config(options)
254+
log.debug('Using config file %r', self._run_conf.config)
255+
if not os.path.exists(self._run_conf.config):
256+
raise IllegalConfiguration('Configuration file {!r} specified '
257+
'via --config was not found.'
258+
.format(self._run_conf.config))
259+
if None in self._cache:
260+
return self._cache[None]
261+
options, _ = self._read_configuration_file(self._run_conf.config)
262+
config = self._create_check_config(options)
236263

237264
# Make the CLI always win
238265
final_config = {}
@@ -244,8 +271,19 @@ def _get_config(self, node):
244271
config = CheckConfiguration(**final_config)
245272

246273
self._set_add_options(config.checked_codes, self._options)
247-
self._cache[path] = config
248-
return self._cache[path]
274+
275+
# Handle caching
276+
if self._run_conf.config is not None:
277+
self._cache[None] = config
278+
else:
279+
self._cache[self._get_node_dir(node)] = config
280+
return config
281+
282+
@staticmethod
283+
def _get_node_dir(node):
284+
"""Return the absolute path of the directory of a filesystem node."""
285+
path = os.path.abspath(node)
286+
return path if os.path.isdir(path) else os.path.dirname(path)
249287

250288
def _read_configuration_file(self, path):
251289
"""Try to read and parse `path` as a configuration file.
@@ -365,7 +403,7 @@ def _get_section_name(cls, parser):
365403
def _get_config_file_in_folder(cls, path):
366404
"""Look for a configuration file in `path`.
367405
368-
If exists return it's full path, otherwise None.
406+
If exists return its full path, otherwise None.
369407
370408
"""
371409
if os.path.isfile(path):
@@ -511,6 +549,8 @@ def _create_option_parser(cls):
511549
help='print status information')
512550
option('--count', action='store_true', default=False,
513551
help='print total number of errors to stdout')
552+
option('--config', metavar='<path>', default=None,
553+
help='use given config file and disable config discovery')
514554

515555
# Error check options
516556
option('--select', metavar='<codes>', default=None,
@@ -571,4 +611,4 @@ class IllegalConfiguration(Exception):
571611
# General configurations for pydocstyle run.
572612
RunConfiguration = namedtuple('RunConfiguration',
573613
('explain', 'source', 'debug',
574-
'verbose', 'count'))
614+
'verbose', 'count', 'config'))

src/tests/test_integration.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(self, script_name='pydocstyle'):
3939
self.tempdir = None
4040
self.script_name = script_name
4141

42-
def write_config(self, prefix='', **kwargs):
42+
def write_config(self, prefix='', name='tox.ini', **kwargs):
4343
"""Change an environment config file.
4444
4545
Applies changes to `tox.ini` relative to `tempdir/prefix`.
@@ -50,7 +50,7 @@ def write_config(self, prefix='', **kwargs):
5050
if not os.path.isdir(base):
5151
self.makedirs(base)
5252

53-
with open(os.path.join(base, 'tox.ini'), 'wt') as conf:
53+
with open(os.path.join(base, name), 'wt') as conf:
5454
conf.write("[{}]\n".format(self.script_name))
5555
for k, v in kwargs.items():
5656
conf.write("{} = {}\n".format(k.replace('_', '-'), v))
@@ -63,6 +63,9 @@ def open(self, path, *args, **kwargs):
6363
"""
6464
return open(os.path.join(self.tempdir, path), *args, **kwargs)
6565

66+
def get_path(self, name, prefix=''):
67+
return os.path.join(self.tempdir, prefix, name)
68+
6669
def makedirs(self, path, *args, **kwargs):
6770
"""Create a directory in a path relative to the environment base."""
6871
os.makedirs(os.path.join(self.tempdir, path), *args, **kwargs)
@@ -248,6 +251,39 @@ def foo():
248251
assert 'D103' not in err
249252

250253

254+
def test_config_path(env):
255+
"""Test that options are correctly loaded from a specific config file.
256+
257+
Make sure that a config file passed via --config is actually used and that
258+
normal config file discovery is disabled.
259+
260+
"""
261+
with env.open('example.py', 'wt') as example:
262+
example.write(textwrap.dedent("""\
263+
def foo():
264+
pass
265+
"""))
266+
267+
env.write_config(ignore='D100')
268+
env.write_config(name='my_config', ignore='D103')
269+
270+
out, err, code = env.invoke()
271+
assert code == 1
272+
assert 'D100' not in out
273+
assert 'D103' in out
274+
275+
out, err, code = env.invoke('--config={} -d'
276+
.format(env.get_path('my_config')))
277+
assert code == 1, out + err
278+
assert 'D100' in out
279+
assert 'D103' not in out
280+
281+
282+
def test_non_existent_config(env):
283+
out, err, code = env.invoke('--config=does_not_exist')
284+
assert code == 2
285+
286+
251287
def test_verbose(env):
252288
"""Test that passing --verbose prints more information."""
253289
with env.open('example.py', 'wt') as example:

0 commit comments

Comments
 (0)