Skip to content

Commit

Permalink
Add development extras check for setup.py files (#16)
Browse files Browse the repository at this point in the history
* Checkpoint

* Complete implementation

* Admit constants

* Add test

* Bump version

* Remove file

* Compatibility with Python < 3.8

* Try with other

* Fix error

* Fix compat

* Debug

* Skip test in Python < 3.8

* Dont support Python < 3.8

* Fix error in test
  • Loading branch information
mondeja committed Mar 10, 2022
1 parent 226a386 commit c1d1761
Show file tree
Hide file tree
Showing 6 changed files with 416 additions and 86 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.6.0
current_version = 1.7.0

[bumpversion:file:setup.cfg]

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
name: dev-extras-required
entry: dev-extras-required-hook
description: Check if your development dependencies contains all other extras requirements
files: '(setup\.cfg)|(pyproject\.toml)'
files: '(setup\.cfg)|(pyproject\.toml)|(setup\.py)'
language: python
- id: nameservers-endswith
name: nameservers-endswith
Expand Down
9 changes: 3 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

```yaml
- repo: https://github.com/mondeja/pre-commit-hooks
rev: v1.5.3
rev: v1.7.0
hooks:
- id: dev-extras-required
- id: root-editorconfig-required
Expand Down Expand Up @@ -41,10 +41,8 @@ Add a pre-commit hook to your configuration file if is not already defined.

### **`dev-extras-required`**

> - Doesn't support `setup.py` files. Please, [migrate your setup configuration
to `setup.cfg` format][setup-py-upgrade-link].
> - Support for `pyproject.toml` files is limited to printing errors, automatic
file rewriting is not performed.
> - Support for `pyproject.toml` and `setup.py` files is limited to
printing errors, automatic file rewriting is not performed.

Check if your development dependencies contains all other extras requirements.
If an extra requirement is defined in other extra group than your development
Expand Down Expand Up @@ -157,6 +155,5 @@ You need to install
[cloudflare-apikey-link]: https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys
[gh-pages-link]: https://pages.github.com
[pre-commit-po-hooks-link]: https://github.com/mondeja/pre-commit-po-hooks
[setup-py-upgrade-link]: https://github.com/asottile/setup-py-upgrade
[repo-stream-link]: https://github.com/mondeja/repo-stream

142 changes: 137 additions & 5 deletions hooks/dev_extras_required.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def check_setup_cfg(
if dry_run:
if not quiet:
sys.stderr.write(
f"Requirement '{requirement}' would be added to"
f"Requirement '{requirement}' must be added to"
f" '{dev_extra_name}' extra group at '{filename}'\n"
)
else:
Expand Down Expand Up @@ -148,7 +148,7 @@ def update_pyproject_toml_if_needed(build_system):
# if dry_run:
if not quiet:
sys.stderr.write(
f"Requirement '{requirement}' would be added to"
f"Requirement '{requirement}' must be added to"
f" '{dev_extra_name}' extra group at '{filename}'\n"
)
"""
Expand Down Expand Up @@ -183,6 +183,122 @@ def update_pyproject_toml_if_needed(build_system):
return exitcode


def check_setup_py(
filename="pyproject.toml",
dev_extra_name="dev",
quiet=False,
):
"""Check that all other extra requirements than development ones are
included in development extra group in a ``setup.py`` configuration
file. Only works if the groups are explicitly defined constants.
Parameters
----------
filename : str, optional
Path to the file to check.
dev_extra_name : str, optional
Development extra requirements group name.
quiet : bool, optional
Enabled, don't print output to stderr when a requirement is not
defined in development extra requirements group.
"""
import ast

# Not compatible with Python < 3.8
if sys.version_info < (3, 8): # pragma: no cover
sys.stderr.write(
"'dev-extras-required' hook for 'setup.py' files only supports"
" Python >= 3.8\n"
)
return 1

with open(filename) as f:
content = f.read()

class ExtraRequirementsExtractor(ast.NodeVisitor):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.extras = None
self.exitcode = 0

def generic_visit(self, node):
if isinstance(node, ast.keyword) and node.arg == "extras_require":
if node.value:
self.extras = self._check_extras(self._extract_extras(node))
ast.NodeVisitor.generic_visit(self, node)

def _extract_extras(self, node):
extras = {}

keys = [const.value for const in node.value.keys]
for i in range(len(keys)):
values = []
for elt in node.value.values[i].elts:
if isinstance(elt, ast.Constant):
values.append(elt.value)
else:
req = elt.id if isinstance(elt, ast.Name) else str(elt)
values.append(req)
extras[keys[i]] = values
return extras

def _check_extras(self, extras):
if self.extras is not None:
if not quiet:
sys.stderr.write(
"Multiple 'extras_require' keywords found"
f" in '{filename}'. Impossible to resolve"
" extras groups.\n"
)
self.exitcode = 1
elif not extras:
sys.stderr.write(
f"Empty extra requirements group found in file '{filename}'\n"
)
self.exitcode = 1

return extras

setup_py_tree = ast.parse(ast.parse(content))
visitor = ExtraRequirementsExtractor()

# Useful for debugging (Python >= 3.9 needed)
# print(ast.dump(setup_py_tree, indent=4))

visitor.visit(setup_py_tree)

if visitor.exitcode == 1:
return visitor.exitcode
elif visitor.extras is None:
sys.stderr.write(f"Extra requirements not found in file '{filename}'\n")
return 1

dev_extra_requirements, other_extra_requirements = ([], [])

for extra, requirements in visitor.extras.items():
if extra == dev_extra_name:
dev_extra_requirements.extend(requirements)
else:
other_extra_requirements.extend(requirements)

exitcode = 0

for requirement in other_extra_requirements:
if requirement not in dev_extra_requirements:
exitcode = 1

if not quiet:
sys.stderr.write(
f"Requirement '{requirement}' must be added to"
f" '{dev_extra_name}' extra group at '{filename}'\n"
)
return exitcode


def main():
parser = argparse.ArgumentParser()
parser.add_argument("filenames", nargs="*")
Expand All @@ -202,7 +318,7 @@ def main():
required=False,
default="dev",
dest="extra_name",
help="Path to the 'setup.cfg' file.",
help="Development extra group name.",
)
parser.add_argument(
"-setup-cfg",
Expand All @@ -224,9 +340,19 @@ def main():
dest="pyproject_toml",
help="Path to the 'pyproject.toml' file.",
)
parser.add_argument(
"-setup-py",
"--setup-py",
type=str,
metavar="FILEPATH",
required=False,
default="setup.py",
dest="setup_py",
help="Path to the 'setup.py' file.",
)
args = parser.parse_args()

filenames = (args.setup_cfg, args.pyproject_toml)
filenames = (args.setup_cfg, args.pyproject_toml, args.setup_py)
if not any([os.path.isfile(filename) for filename in filenames]):
for filename in filenames:
sys.stderr.write(f"'{filename}' file not found\n")
Expand All @@ -239,12 +365,18 @@ def main():
quiet=args.quiet,
dry_run=args.dry_run,
)
else:
elif os.path.isfile(args.pyproject_toml):
exitcode = check_pyproject_toml(
filename=args.pyproject_toml,
dev_extra_name=args.extra_name,
quiet=args.quiet,
)
else:
exitcode = check_setup_py(
filename=args.setup_py,
dev_extra_name=args.extra_name,
quiet=args.quiet,
)

return exitcode

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = mondeja_pre_commit_hooks
version = 1.6.0
version = 1.7.0
description = My own useful pre-commit hooks
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down

0 comments on commit c1d1761

Please sign in to comment.