Skip to content

Commit

Permalink
Add autofixer for upgrading setup.py
Browse files Browse the repository at this point in the history
  • Loading branch information
asottile committed Feb 18, 2019
1 parent 6252173 commit 7d5781e
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,10 @@ You can find some more involved examples in [all_repos/autofix](https://github.c
[pre-commit/pre-commit-hooks] to [pycqa/flake8].
- `all_repos.autofix.pre_commit_migrate_config`: runs
`pre-commit migrate-config`.
- `all_repos.autofix.setup_py_upgrade`: runs [setup-py-upgrade] and then
tidies up the metadata fields and adds a few missing ones.

[pre-commit/pre-commit-hooks]: https://github.com/pre-commit/pre-commit-hooks
[mirrors-autopep8]: https://github.com/pre-commit/mirrors-autopep8
[pycqa/flake8]: https://gitlab.com/pycqa/flake8
[setup-py-upgrade]: https://github.com/asottile/setup-py-upgrade
135 changes: 135 additions & 0 deletions all_repos/autofix/setup_py_upgrade.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import argparse
import configparser
import io
import os.path
import sys
from typing import Dict
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple

from all_repos import autofix_lib
from all_repos.config import Config
from all_repos.grep import repos_matching

KEYS_ORDER: Tuple[Tuple[str, Tuple[str, ...]], ...] = (
(
'metadata', (
'name', 'version', 'description',
'long_description', 'long_description_content_type',
'url', 'author', 'author_email', 'license', 'license_file',
'platforms', 'classifiers',
),
),
(
'options', (
'packages', 'py_modules', 'install_requires', 'python_requires',
),
),
('options.sections.find', ('where', 'exclude', 'include')),
('options.entry_points', ('console_scripts',)),
('options.extras_require', ()),
('options.package_data', ()),
('options.exclude_package_data', ()),
)


def find_repos(config: Config) -> Set[str]:
return repos_matching(config, ('=', '--', 'setup.py'))


def apply_fix() -> None:
autofix_lib.run(sys.executable, '-m', 'setup_py_upgrade', '.')

cfg = configparser.ConfigParser()
cfg.read('setup.cfg')

# normalize names to underscores so sdist / wheel have the same prefix
cfg['metadata']['name'] = cfg['metadata']['name'].replace('-', '_')

# if README.md exists, set `long_description` + content type
if os.path.exists('README.md'):
cfg['metadata']['long_description'] = 'file: README.md'
cfg['metadata']['long_description_content_type'] = 'text/markdown'

if os.path.exists('LICENSE'):
cfg['metadata']['license_file'] = 'LICENSE'

with open('LICENSE') as f:
contents = f.read()

# TODO: pick a better way to identify licenses
if 'Permission is hereby granted, free of charge, to any' in contents:
cfg['metadata']['license'] = 'MIT'
cfg['metadata']['classifiers'] = (
cfg['metadata'].get('classifiers', '').rstrip() +
'\nLicense :: OSI Approved :: MIT License'
)

# TODO:
# add the necessary python classifiers
# `python_requires`

if 'classifiers' in cfg['metadata']:
classifiers = sorted(set(cfg['metadata']['classifiers'].split('\n')))
cfg['metadata']['classifiers'] = '\n'.join(classifiers)

sections: Dict[str, Dict[str, str]] = {}
for section, key_order in KEYS_ORDER:
if section not in cfg:
continue

new_section = {
k: cfg[section].pop(k) for k in key_order if k in cfg[section]
}
# sort any remaining keys
new_section.update(sorted(cfg[section].items()))

sections[section] = new_section
cfg.pop(section)

for section in cfg.sections():
sections[section] = dict(cfg[section])
cfg.pop(section)

for k, v in sections.items():
cfg[k] = v

sio = io.StringIO()
cfg.write(sio)
with open('setup.cfg', 'w') as f:
contents = sio.getvalue().strip() + '\n'
contents = contents.replace('\t', ' ')
contents = contents.replace(' \n', '\n')
f.write(contents)


def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
autofix_lib.add_fixer_args(parser)
args = parser.parse_args(argv)

autofix_lib.assert_importable(
'setup_py_upgrade', install='setup-py-upgrade',
)

repos, config, commit, autofix_settings = autofix_lib.from_cli(
args,
find_repos=find_repos,
msg='Migrate setup.py to setup.cfg declarative metadata',
branch_name='setup-py-upgrade',
)

autofix_lib.fix(
repos,
apply_fix=apply_fix,
config=config,
commit=commit,
autofix_settings=autofix_settings,
)
return 0


if __name__ == '__main__':
exit(main())
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ flake8
pre-commit
pytest
pytest-env
setup-py-upgrade>=0.0.4
142 changes: 142 additions & 0 deletions tests/autofix/setup_py_upgrade_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import subprocess

import pytest

from all_repos import clone
from all_repos.autofix.setup_py_upgrade import find_repos
from all_repos.autofix.setup_py_upgrade import main
from all_repos.config import load_config
from testing.auto_namedtuple import auto_namedtuple
from testing.git import init_repo
from testing.git import write_file_commit


@pytest.fixture
def setup_py_repo(tmpdir):
src_repo = tmpdir.join('hook_repo')
init_repo(src_repo)
src_repo.join('setup.cfg').write('[bdist_wheel]\nuniversal = true\n')
write_file_commit(
src_repo, 'setup.py',
'from setuptools import setup\n'
'setup(name="pkg", version="1.0")\n',
)

update_repo = tmpdir.join('update_repo')
subprocess.check_call(('git', 'clone', src_repo, update_repo))

return auto_namedtuple(src_repo=src_repo, update_repo=update_repo)


def test_basic_rewrite(file_config, setup_py_repo):
ret = main((
'--config-filename', str(file_config.cfg),
'--repos', str(setup_py_repo.update_repo),
))
assert not ret

setup_py = setup_py_repo.src_repo.join('setup.py').read()
assert setup_py == 'from setuptools import setup\nsetup()\n'
setup_cfg = setup_py_repo.src_repo.join('setup.cfg').read()
assert setup_cfg == (
'[metadata]\n'
'name = pkg\n'
'version = 1.0\n'
'\n'
'[bdist_wheel]\n'
'universal = true\n'
)


def test_rewrite_with_readme(file_config, setup_py_repo):
write_file_commit(setup_py_repo.src_repo, 'README.md', 'my project!')
ret = main((
'--config-filename', str(file_config.cfg),
'--repos', str(setup_py_repo.update_repo),
))
assert not ret

setup_py = setup_py_repo.src_repo.join('setup.py').read()
assert setup_py == 'from setuptools import setup\nsetup()\n'
setup_cfg = setup_py_repo.src_repo.join('setup.cfg').read()
assert setup_cfg == (
'[metadata]\n'
'name = pkg\n'
'version = 1.0\n'
'long_description = file: README.md\n'
'long_description_content_type = text/markdown\n'
'\n'
'[bdist_wheel]\n'
'universal = true\n'
)


def test_rewrite_with_license(file_config, setup_py_repo):
with open('LICENSE') as f:
contents = f.read()
write_file_commit(setup_py_repo.src_repo, 'LICENSE', contents)
ret = main((
'--config-filename', str(file_config.cfg),
'--repos', str(setup_py_repo.update_repo),
))
assert not ret

setup_py = setup_py_repo.src_repo.join('setup.py').read()
assert setup_py == 'from setuptools import setup\nsetup()\n'
setup_cfg = setup_py_repo.src_repo.join('setup.cfg').read()
assert setup_cfg == (
'[metadata]\n'
'name = pkg\n'
'version = 1.0\n'
'license = MIT\n'
'license_file = LICENSE\n'
'classifiers =\n'
' License :: OSI Approved :: MIT License\n'
'\n'
'[bdist_wheel]\n'
'universal = true\n'
)


def test_rewrite_with_unknown_license(file_config, setup_py_repo):
write_file_commit(
setup_py_repo.src_repo, 'LICENSE',
'Copyright (C) asottile, definitely not a real license',
)
ret = main((
'--config-filename', str(file_config.cfg),
'--repos', str(setup_py_repo.update_repo),
))
assert not ret

setup_py = setup_py_repo.src_repo.join('setup.py').read()
assert setup_py == 'from setuptools import setup\nsetup()\n'
setup_cfg = setup_py_repo.src_repo.join('setup.cfg').read()
assert setup_cfg == (
'[metadata]\n'
'name = pkg\n'
'version = 1.0\n'
'license_file = LICENSE\n'
'\n'
'[bdist_wheel]\n'
'universal = true\n'
)


def test_find_repos_skips_already_migrated(file_config_files):
write_file_commit(
file_config_files.dir1, 'setup.py',
'from setuptools import setup\nsetup()\n',
)
clone.main(('--config-filename', str(file_config_files.cfg)))
assert find_repos(load_config(str(file_config_files.cfg))) == set()


def test_find_repos_finds_a_repo(file_config_files):
write_file_commit(
file_config_files.dir1, 'setup.py',
'from setuptools import setup\nsetup(name="pkg", version="1")\n',
)
clone.main(('--config-filename', str(file_config_files.cfg)))
ret = find_repos(load_config(str(file_config_files.cfg)))
assert ret == {str(file_config_files.output_dir.join('repo1'))}

0 comments on commit 7d5781e

Please sign in to comment.