Skip to content

Commit

Permalink
Parse options from config file (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 authored May 31, 2023
1 parent cc57ff5 commit 14e0ce7
Show file tree
Hide file tree
Showing 11 changed files with 256 additions and 13 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
- Added
- A new option to allow no return section in the docstring if the function
implicitly returns `None`
- Changed
- Made pydoclint options configurable via a config file (both in the native
mode and in the flake8 plugin mode)

## [0.0.4] - 2023-05-27

Expand Down
54 changes: 49 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,60 @@ other built-in _flake8_ linters on your code.
Should I use _pydoclint_ as a native command line tool or a _flake8_ plugin?
Here's comparison:

| | Pros | Cons |
| --------------- | ------------------------------------------ | ------------------------------------------------------------- |
| Native tool | Slightly faster | No per-line or project-wide omission support right now [*] |
| _flake8_ plugin | Supports per-line or project-wide omission | Slightly slower because other flake8 plugins are run together |
| | Pros | Cons |
| --------------- | ---------------------------------------- | ------------------------------------------------------------- |
| Native tool | Slightly faster | No inline or project-wide omission support right now [*] |
| _flake8_ plugin | Supports inline or project-wide omission | Slightly slower because other flake8 plugins are run together |

\*) This feature may be added in the near future

### 2.4. Configuration

Here is how to configure _pydoclint_. For detailed explanations of all options,
see [Section 4](#4-configuration-options) below.

#### 2.4.1. Setting options inline

- Native:

```bash
pydoclint --check-arg-order=False <FILE_OR_FOLDER_PATH>
```

- Flake8:

```bash
flake8 --check-arg-order=False <FILE_OR_FOLDER_PATH>
```

#### 2.4.2. Setting options in a configuration file

- Native:

- In a `.toml` file somewhere in your project folder, add a section like this
(put in the config that you need):

```toml
[tool.pydoclint]
style = 'google'
exclude = '\.git|\.tox|tests/data|some_script\.py'
require-return-section-when-returning-none = true
```

- Then, specify the path of the `.toml` file in your command:

```bash
pydoclint --config=path/to/my/config.toml <FILE_OR_FOLDER_PATH>
```

- Flake8:
- In your flake8 config file (see
[flake8's official doc](https://flake8.pycqa.org/en/latest/user/configuration.html#configuration-locations)),
add the config you need under the section `[flake8]`
## 3. Style violation codes
`pydoclint` currently has the following style violation codes:
_pydoclint_ currently has the following style violation codes:
### 3.1. `DOC1xx`: Violations about input arguments
Expand Down
1 change: 0 additions & 1 deletion pydoclint/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
__version__ = '0.1.0'
7 changes: 7 additions & 0 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,23 @@ def add_options(cls, parser): # noqa: D102
'--style',
action='store',
default='numpy',
parse_from_config=True,
help='Which style of docstring is your code base using',
)
parser.add_option(
'-th',
'--check-type-hint',
action='store',
default='True',
parse_from_config=True,
help='Whether to check type hints in the docstring',
)
parser.add_option(
'-ao',
'--check-arg-order',
action='store',
default='True',
parse_from_config=True,
help=(
'Whether to check argument orders in the docstring'
' against the argument list in the function signature'
Expand All @@ -44,27 +47,31 @@ def add_options(cls, parser): # noqa: D102
'--skip-checking-short-docstrings',
action='store',
default='True',
parse_from_config=True,
help='If True, skip checking if the docstring only has a short summary.',
)
parser.add_option(
'-scr',
'--skip-checking-raises',
action='store',
default='False',
parse_from_config=True,
help='If True, skip checking docstring "Raises" section against "raise" statements',
)
parser.add_option(
'-aid',
'--allow-init-docstring',
action='store',
default='False',
parse_from_config=True,
help='If True, allow both __init__() and the class def to have docstrings',
)
parser.add_option(
'-rrs',
'--require-return-section-when-returning-none',
action='store',
default='False',
parse_from_config=True,
help=(
'If False, a return section is not needed in docstring if'
' the function body does not have a "return" statement and'
Expand Down
24 changes: 23 additions & 1 deletion pydoclint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

import click

from pydoclint.parse_config import (
injectDefaultOptionsFromUserSpecifiedTomlFilePath,
)
from pydoclint.utils.violation import Violation
from pydoclint.visitor import Visitor

Expand Down Expand Up @@ -121,8 +124,26 @@ def validateStyleValue(
),
is_eager=True,
)
@click.option(
'--config',
type=click.Path(
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
allow_dash=False,
path_type=str,
),
is_eager=True,
callback=injectDefaultOptionsFromUserSpecifiedTomlFilePath,
help=(
'The full path of the .toml config file that contains the config'
' options; note that the command line options take precedence'
' over the .toml file'
),
)
@click.pass_context
def main(
def main( # noqa: C901
ctx: click.Context,
quiet: bool,
exclude: str,
Expand All @@ -135,6 +156,7 @@ def main(
skip_checking_raises: bool,
allow_init_docstring: bool,
require_return_section_when_returning_none: bool,
config: Optional[str],
) -> None:
"""Command-line entry point of pydoclint"""
ctx.ensure_object(dict)
Expand Down
112 changes: 112 additions & 0 deletions pydoclint/parse_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import sys
from pathlib import Path
from typing import Any, Dict, Optional, Sequence

import click

if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib


def injectDefaultOptionsFromUserSpecifiedTomlFilePath(
ctx: click.Context,
param: click.Parameter,
value: Optional[str],
) -> Optional[str]:
"""
Inject default objects from user-specified .toml file path.
Parameters
----------
ctx : click.Context
The "click" context
param : click.Parameter
The "click" parameter; not used in this function; just a placeholder
value : Optional[str]
The full path of the .toml file. (It needs to be named ``value``
so that ``click`` can correctly use it as a callback function.)
Returns
-------
Optional[str]
The full path of the .toml file
"""
if not value:
return None

print(f'Loading config from user-specified .toml file: {value}')
config = parseOneTomlFile(tomlFilename=Path(value))
updateCtxDefaultMap(ctx=ctx, config=config)
return value


def parseToml(paths: Optional[Sequence[str]]) -> Dict[str, Any]:
"""Parse the pyproject.toml located in the common parent of ``paths``"""
if paths is None:
return {}

commonParent: Path = findCommonParentFolder(paths)
tomlFilename = commonParent / Path('pyproject.toml')
print(f'Loading config from inferred .toml file path: {tomlFilename}')
return parseOneTomlFile(tomlFilename)


def parseOneTomlFile(tomlFilename: Path) -> Dict[str, Any]:
"""Parse a .toml file"""
if not tomlFilename.exists():
print(f'File "{tomlFilename}" does not exist; nothing to load.')
return {}

try:
with open(tomlFilename, 'rb') as fp:
rawConfig = tomllib.load(fp)

pydoclintSection = rawConfig['tool']['pydoclint']
finalConfig = {
k.replace('-', '_'): v for k, v in pydoclintSection.items()
}
except Exception:
finalConfig = {}

if len(finalConfig) > 0:
print(f'Found options defined in {tomlFilename}:')
print(finalConfig)
else:
print(f'No config found in {tomlFilename}.')

return finalConfig


def findCommonParentFolder(
paths: Sequence[str],
makeAbsolute: bool = True, # allow makeAbsolute=False just for testing
) -> Path:
"""Find the common parent folder of the given ``paths``"""
paths = [Path(path) for path in paths]

common_parent = paths[0]
for path in paths[1:]:
if len(common_parent.parts) > len(path.parts):
common_parent, path = path, common_parent

for i, part in enumerate(common_parent.parts):
if part != path.parts[i]:
common_parent = Path(*common_parent.parts[:i])
break

if makeAbsolute:
return common_parent.absolute()

return common_parent


def updateCtxDefaultMap(ctx: click.Context, config: Dict[str, Any]) -> None:
"""Update the ``click`` context default map with the provided ``config``"""
default_map: Dict[str, Any] = {}
if ctx.default_map:
default_map.update(ctx.default_map)

default_map.update(config)
ctx.default_map = default_map
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ skip = ['unparser.py']

[tool.cercis]
wrap-line-with-long-string = true

[tool.pydoclint]
style = 'numpy'
exclude = '\.git|\.tox|tests/data|unparser\.py'
require-return-section-when-returning-none = true
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ install_requires =
click>=8.0.0
numpydoc>=1.5.0
docstring_parser>=0.15
tomli>=2.0.1; python_version<'3.11'
python_requires = >=3.8

[options.packages.find]
Expand Down
7 changes: 7 additions & 0 deletions tests/data/example_config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[tool.mytool]
option1 = true
option2 = 'hello'

[tool.pydoclint]
style = 'google'
some-custom-option = true
43 changes: 43 additions & 0 deletions tests/test_parse_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from pathlib import Path
from typing import Any, Dict, List

import pytest

from pydoclint.parse_config import findCommonParentFolder, parseOneTomlFile

THIS_DIR = Path(__file__).parent
DATA_DIR = THIS_DIR / 'data'


@pytest.mark.parametrize(
'paths, expected',
[
(['/a/b/c', '/a/b/d', '/a/b/e/f/g'], '/a/b'),
(['a/b/c', 'a/b/d', 'a/b/e/f/g'], 'a/b'),
(['/a/b/c', '/a/b/d', '/a/b/e/f/g/file.txt'], '/a/b'),
(['/a/b/c', '/e/f/g', '/a/b/e/f/g'], '/'),
(['~/a/b/c', '~/e/f/g', '~/a/b/e/f/g'], '~'),
(['a/b/c', 'e/f/g', 'a/b/e/f/g'], '.'),
(['a/b/c', 'a/b/d', 'a/b/e/f/g'], 'a/b'),
(['./a/b/c', './a/b/d', './a/b/e/f/g'], 'a/b'),
(['./a/b/c', './e/f/g', './a/b/e/f/g'], '.'),
],
)
def testFindCommonParentFolder(paths: List[str], expected: str) -> None:
result = findCommonParentFolder(paths, makeAbsolute=False).as_posix()
assert result == expected


@pytest.mark.parametrize(
'filename, expected',
[
(Path('a_path_that_doesnt_exist.toml'), {}),
(
DATA_DIR / 'example_config.toml',
{'some_custom_option': True, 'style': 'google'},
),
],
)
def testParseOneTomlFile(filename: Path, expected: Dict[str, Any]) -> None:
tomlConfig = parseOneTomlFile(filename)
assert tomlConfig == expected
12 changes: 6 additions & 6 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[tox]
envlist =
cercis
flake8-basic
flake8-misc
flake8-docstrings
py38
py39
py310
py311
cercis
check-self
flake8-basic
flake8-misc
flake8-docstrings


[testenv:cercis]
Expand Down Expand Up @@ -51,7 +51,7 @@ skip_install = true
deps =
flake8-docstrings
commands =
flake8 --ignore D100,D104,D105,D107,D400,D205 ./pydoclint
flake8 --ignore D100,D104,D105,D107,D400,D205 --select D ./pydoclint


[testenv]
Expand All @@ -63,7 +63,7 @@ commands =
[testenv:check-self]
commands =
pip install -e .
pydoclint --exclude='\.git|\.tox|tests/data|unparser\.py' .
pydoclint --config=pyproject.toml .


[flake8]
Expand Down

0 comments on commit 14e0ce7

Please sign in to comment.