Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse options from config file #11

Merged
merged 7 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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