-
-
Notifications
You must be signed in to change notification settings - Fork 84
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add autofixer for upgrading setup.py
- Loading branch information
Showing
4 changed files
with
281 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ flake8 | |
pre-commit | ||
pytest | ||
pytest-env | ||
setup-py-upgrade>=0.0.4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'))} |